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 = {
|
||||
meta: {
|
||||
docs: {
|
||||
|
|
@ -1286,6 +1325,7 @@ const plugin = {
|
|||
'no-inline-named-types': noInlineNamedTypes,
|
||||
'no-structuredclone-scene-arrays': noStructuredCloneSceneArrays,
|
||||
'no-vue-style-blocks': noVueStyleBlocks,
|
||||
'no-invalid-test-id-attributes': noInvalidTestIdAttributes,
|
||||
'no-document-query-selector-in-vue': noDocumentQuerySelectorInVue,
|
||||
'no-direct-selection-tool-state-mutation': noDirectSelectionToolStateMutation,
|
||||
'no-math-random': noMathRandom,
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@
|
|||
],
|
||||
"open-pencil/no-structuredclone-scene-arrays": "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-direct-selection-tool-state-mutation": "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 { usePropScrub } from '#vue/controls/prop-scrub/use'
|
||||
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. */
|
||||
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: {
|
||||
baseURL: 'http://localhost:1420',
|
||||
testIdAttribute: 'data-test-id',
|
||||
viewport: { width: 1280, height: 800 },
|
||||
deviceScaleFactor: 2,
|
||||
colorScheme: 'dark',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<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'
|
||||
|
||||
type PickerSliderDisplay = {
|
||||
|
|
@ -11,6 +11,21 @@ type PickerSliderDisplay = {
|
|||
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 {
|
||||
label,
|
||||
modelValue,
|
||||
|
|
@ -23,21 +38,7 @@ const {
|
|||
thumbFill = '#fff',
|
||||
testId,
|
||||
ui
|
||||
} = defineProps<{
|
||||
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>
|
||||
>
|
||||
}>()
|
||||
} = defineProps<PickerSliderProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
|
|
@ -63,7 +64,7 @@ function thumbLeft(): string {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cls.root" :data-test-id="testId">
|
||||
<div :class="cls.root" v-bind="testIdAttr(testId)">
|
||||
<span :class="cls.label">{{ label }}</span>
|
||||
<div :class="cls.track">
|
||||
<div :class="cls.gradient" :style="gradientStyle" />
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectViewport
|
||||
} from 'reka-ui'
|
||||
import { testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
|
||||
|
||||
import { useSelectUI } from '@/components/ui/select'
|
||||
|
||||
|
|
@ -32,12 +33,13 @@ interface GroupedSelectUi {
|
|||
separator?: string
|
||||
}
|
||||
|
||||
const { groups, displayValue, ui, testId } = defineProps<{
|
||||
groups: SelectGroupDef<T>[]
|
||||
displayValue: string
|
||||
ui?: GroupedSelectUi
|
||||
testId?: string
|
||||
}>()
|
||||
const { groups, displayValue, ui, testId } = defineProps<
|
||||
TestIdProps & {
|
||||
groups: SelectGroupDef<T>[]
|
||||
displayValue: string
|
||||
ui?: GroupedSelectUi
|
||||
}
|
||||
>()
|
||||
|
||||
const modelValue = defineModel<T>({ required: true })
|
||||
|
||||
|
|
@ -55,7 +57,7 @@ const separator = ui?.separator ?? 'mx-1 my-1 h-px bg-border'
|
|||
|
||||
<template>
|
||||
<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>
|
||||
<icon-lucide-chevron-down class="size-2.5 shrink-0 text-muted" />
|
||||
</SelectTrigger>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import {
|
|||
SelectViewport
|
||||
} from 'reka-ui'
|
||||
|
||||
import { testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
|
||||
|
||||
import { useSelectUI } from '@/components/ui/select'
|
||||
import type { SelectUi } from '@/components/ui/select'
|
||||
|
||||
|
|
@ -24,12 +26,13 @@ const {
|
|||
placeholder,
|
||||
ui,
|
||||
testId = 'app-select-trigger'
|
||||
} = defineProps<{
|
||||
options: { value: T; label: string }[]
|
||||
placeholder?: string
|
||||
ui?: AppSelectUi
|
||||
testId?: string
|
||||
}>()
|
||||
} = defineProps<
|
||||
TestIdProps & {
|
||||
options: { value: T; label: string }[]
|
||||
placeholder?: string
|
||||
ui?: AppSelectUi
|
||||
}
|
||||
>()
|
||||
|
||||
const modelValue = defineModel<T>({ required: true })
|
||||
|
||||
|
|
@ -44,7 +47,7 @@ const indicator = ui?.indicator ?? 'absolute left-1.5 inline-flex items-center j
|
|||
|
||||
<template>
|
||||
<SelectRoot v-model="modelValue">
|
||||
<SelectTrigger :data-test-id="testId" :class="select.trigger">
|
||||
<SelectTrigger v-bind="testIdAttr(testId)" :class="select.trigger">
|
||||
<SelectValue :placeholder="placeholder" />
|
||||
<icon-lucide-chevron-down class="ml-1 size-3 shrink-0 text-muted" />
|
||||
</SelectTrigger>
|
||||
|
|
|
|||
Loading…
Reference in a new issue