chore(vue): standardize test id helpers
This commit is contained in:
parent
e2eaf85cff
commit
0749bfa755
|
|
@ -201,6 +201,45 @@ const noVueStyleBlocks = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TEST_ID_FORMAT = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
||||||
|
|
||||||
|
const noInvalidTestIdAttributes = {
|
||||||
|
meta: {
|
||||||
|
docs: {
|
||||||
|
description: 'Enforce data-test-id spelling and kebab-case static test ids in Vue components'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const file = normalizedFilename(context)
|
||||||
|
if (!file.endsWith('.vue')) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Program(node) {
|
||||||
|
const source = context.sourceCode.getText()
|
||||||
|
const invalidAttr = source.match(/\bdata-testid\s*=/)
|
||||||
|
if (invalidAttr) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Use data-test-id instead of data-testid.'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrPattern = /\bdata-test-id\s*=\s*"([^"]+)"/g
|
||||||
|
for (const match of source.matchAll(attrPattern)) {
|
||||||
|
const id = match[1]
|
||||||
|
if (TEST_ID_FORMAT.test(id)) continue
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: `Static data-test-id values must be kebab-case. Invalid id: "${id}".`
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const noDocumentQuerySelectorInVue = {
|
const noDocumentQuerySelectorInVue = {
|
||||||
meta: {
|
meta: {
|
||||||
docs: {
|
docs: {
|
||||||
|
|
@ -1286,6 +1325,7 @@ const plugin = {
|
||||||
'no-inline-named-types': noInlineNamedTypes,
|
'no-inline-named-types': noInlineNamedTypes,
|
||||||
'no-structuredclone-scene-arrays': noStructuredCloneSceneArrays,
|
'no-structuredclone-scene-arrays': noStructuredCloneSceneArrays,
|
||||||
'no-vue-style-blocks': noVueStyleBlocks,
|
'no-vue-style-blocks': noVueStyleBlocks,
|
||||||
|
'no-invalid-test-id-attributes': noInvalidTestIdAttributes,
|
||||||
'no-document-query-selector-in-vue': noDocumentQuerySelectorInVue,
|
'no-document-query-selector-in-vue': noDocumentQuerySelectorInVue,
|
||||||
'no-direct-selection-tool-state-mutation': noDirectSelectionToolStateMutation,
|
'no-direct-selection-tool-state-mutation': noDirectSelectionToolStateMutation,
|
||||||
'no-math-random': noMathRandom,
|
'no-math-random': noMathRandom,
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@
|
||||||
],
|
],
|
||||||
"open-pencil/no-structuredclone-scene-arrays": "error",
|
"open-pencil/no-structuredclone-scene-arrays": "error",
|
||||||
"open-pencil/no-vue-style-blocks": "error",
|
"open-pencil/no-vue-style-blocks": "error",
|
||||||
|
"open-pencil/no-invalid-test-id-attributes": "error",
|
||||||
"open-pencil/no-document-query-selector-in-vue": "error",
|
"open-pencil/no-document-query-selector-in-vue": "error",
|
||||||
"open-pencil/no-direct-selection-tool-state-mutation": "error",
|
"open-pencil/no-direct-selection-tool-state-mutation": "error",
|
||||||
"open-pencil/no-math-random": "error",
|
"open-pencil/no-math-random": "error",
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ export { useToolbarState } from '#vue/primitives/Toolbar/useToolbarState'
|
||||||
export { useNodeFontStatus } from '#vue/shared/font-status/use'
|
export { useNodeFontStatus } from '#vue/shared/font-status/use'
|
||||||
export { usePropScrub } from '#vue/controls/prop-scrub/use'
|
export { usePropScrub } from '#vue/controls/prop-scrub/use'
|
||||||
export { toolCursor } from '#vue/editor/tool-cursor'
|
export { toolCursor } from '#vue/editor/tool-cursor'
|
||||||
|
export { testId, testIdSelector } from '#vue/testing/test-id'
|
||||||
|
export type { TestId, TestIdProps } from '#vue/testing/test-id'
|
||||||
|
|
||||||
/** Property-panel composables. */
|
/** Property-panel composables. */
|
||||||
export { usePosition } from '#vue/controls/position/use'
|
export { usePosition } from '#vue/controls/position/use'
|
||||||
|
|
|
||||||
17
packages/vue/src/testing/test-id.ts
Normal file
17
packages/vue/src/testing/test-id.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
export type TestId = string
|
||||||
|
|
||||||
|
export type TestIdProps = {
|
||||||
|
testId?: TestId
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testId(id?: TestId | null): { 'data-test-id'?: TestId } {
|
||||||
|
return id ? { 'data-test-id': id } : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testIdSelector(id: TestId): string {
|
||||||
|
return `[data-test-id="${cssEscape(id)}"]`
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssEscape(value: string): string {
|
||||||
|
return CSS.escape(value)
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:1420',
|
baseURL: 'http://localhost:1420',
|
||||||
|
testIdAttribute: 'data-test-id',
|
||||||
viewport: { width: 1280, height: 800 },
|
viewport: { width: 1280, height: 800 },
|
||||||
deviceScaleFactor: 2,
|
deviceScaleFactor: 2,
|
||||||
colorScheme: 'dark',
|
colorScheme: 'dark',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inputNumberValue } from '@open-pencil/vue'
|
import { inputNumberValue, testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
|
||||||
import { usePickerSliderUI } from './ui/picker-slider'
|
import { usePickerSliderUI } from './ui/picker-slider'
|
||||||
|
|
||||||
type PickerSliderDisplay = {
|
type PickerSliderDisplay = {
|
||||||
|
|
@ -11,6 +11,21 @@ type PickerSliderDisplay = {
|
||||||
parse?: (value: number) => number
|
parse?: (value: number) => number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PickerSliderProps = TestIdProps & {
|
||||||
|
label: string
|
||||||
|
modelValue: number
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
step?: number
|
||||||
|
display?: PickerSliderDisplay
|
||||||
|
gradientStyle?: string
|
||||||
|
checkerboard?: boolean
|
||||||
|
thumbFill?: string
|
||||||
|
ui?: Partial<
|
||||||
|
Record<'root' | 'label' | 'track' | 'gradient' | 'range' | 'thumb' | 'input', string>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
modelValue,
|
modelValue,
|
||||||
|
|
@ -23,21 +38,7 @@ const {
|
||||||
thumbFill = '#fff',
|
thumbFill = '#fff',
|
||||||
testId,
|
testId,
|
||||||
ui
|
ui
|
||||||
} = defineProps<{
|
} = defineProps<PickerSliderProps>()
|
||||||
label: string
|
|
||||||
modelValue: number
|
|
||||||
min: number
|
|
||||||
max: number
|
|
||||||
step?: number
|
|
||||||
display?: PickerSliderDisplay
|
|
||||||
gradientStyle?: string
|
|
||||||
checkerboard?: boolean
|
|
||||||
thumbFill?: string
|
|
||||||
testId?: string
|
|
||||||
ui?: Partial<
|
|
||||||
Record<'root' | 'label' | 'track' | 'gradient' | 'range' | 'thumb' | 'input', string>
|
|
||||||
>
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: number]
|
'update:modelValue': [value: number]
|
||||||
|
|
@ -63,7 +64,7 @@ function thumbLeft(): string {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="cls.root" :data-test-id="testId">
|
<div :class="cls.root" v-bind="testIdAttr(testId)">
|
||||||
<span :class="cls.label">{{ label }}</span>
|
<span :class="cls.label">{{ label }}</span>
|
||||||
<div :class="cls.track">
|
<div :class="cls.track">
|
||||||
<div :class="cls.gradient" :style="gradientStyle" />
|
<div :class="cls.gradient" :style="gradientStyle" />
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectViewport
|
SelectViewport
|
||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
|
import { testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
|
||||||
|
|
||||||
import { useSelectUI } from '@/components/ui/select'
|
import { useSelectUI } from '@/components/ui/select'
|
||||||
|
|
||||||
|
|
@ -32,12 +33,13 @@ interface GroupedSelectUi {
|
||||||
separator?: string
|
separator?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { groups, displayValue, ui, testId } = defineProps<{
|
const { groups, displayValue, ui, testId } = defineProps<
|
||||||
groups: SelectGroupDef<T>[]
|
TestIdProps & {
|
||||||
displayValue: string
|
groups: SelectGroupDef<T>[]
|
||||||
ui?: GroupedSelectUi
|
displayValue: string
|
||||||
testId?: string
|
ui?: GroupedSelectUi
|
||||||
}>()
|
}
|
||||||
|
>()
|
||||||
|
|
||||||
const modelValue = defineModel<T>({ required: true })
|
const modelValue = defineModel<T>({ required: true })
|
||||||
|
|
||||||
|
|
@ -55,7 +57,7 @@ const separator = ui?.separator ?? 'mx-1 my-1 h-px bg-border'
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectRoot v-model="modelValue">
|
<SelectRoot v-model="modelValue">
|
||||||
<SelectTrigger :data-test-id="testId" :class="select.trigger">
|
<SelectTrigger v-bind="testIdAttr(testId)" :class="select.trigger">
|
||||||
<slot name="value">{{ displayValue }}</slot>
|
<slot name="value">{{ displayValue }}</slot>
|
||||||
<icon-lucide-chevron-down class="size-2.5 shrink-0 text-muted" />
|
<icon-lucide-chevron-down class="size-2.5 shrink-0 text-muted" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import {
|
||||||
SelectViewport
|
SelectViewport
|
||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
|
|
||||||
|
import { testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
|
||||||
|
|
||||||
import { useSelectUI } from '@/components/ui/select'
|
import { useSelectUI } from '@/components/ui/select'
|
||||||
import type { SelectUi } from '@/components/ui/select'
|
import type { SelectUi } from '@/components/ui/select'
|
||||||
|
|
||||||
|
|
@ -24,12 +26,13 @@ const {
|
||||||
placeholder,
|
placeholder,
|
||||||
ui,
|
ui,
|
||||||
testId = 'app-select-trigger'
|
testId = 'app-select-trigger'
|
||||||
} = defineProps<{
|
} = defineProps<
|
||||||
options: { value: T; label: string }[]
|
TestIdProps & {
|
||||||
placeholder?: string
|
options: { value: T; label: string }[]
|
||||||
ui?: AppSelectUi
|
placeholder?: string
|
||||||
testId?: string
|
ui?: AppSelectUi
|
||||||
}>()
|
}
|
||||||
|
>()
|
||||||
|
|
||||||
const modelValue = defineModel<T>({ required: true })
|
const modelValue = defineModel<T>({ required: true })
|
||||||
|
|
||||||
|
|
@ -44,7 +47,7 @@ const indicator = ui?.indicator ?? 'absolute left-1.5 inline-flex items-center j
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectRoot v-model="modelValue">
|
<SelectRoot v-model="modelValue">
|
||||||
<SelectTrigger :data-test-id="testId" :class="select.trigger">
|
<SelectTrigger v-bind="testIdAttr(testId)" :class="select.trigger">
|
||||||
<SelectValue :placeholder="placeholder" />
|
<SelectValue :placeholder="placeholder" />
|
||||||
<icon-lucide-chevron-down class="ml-1 size-3 shrink-0 text-muted" />
|
<icon-lucide-chevron-down class="ml-1 size-3 shrink-0 text-muted" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue