refactor(vue): use test id directive

This commit is contained in:
Danila Poyarkov 2026-05-15 12:37:00 +03:00
parent 50a4e1bf93
commit 744f0a0f42
19 changed files with 57 additions and 38 deletions

View file

@ -48,6 +48,7 @@ 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 { vTestId } from '#vue/testing/v-test-id'
export type {
RequiredTestIdProps,
TestId,

View file

@ -0,0 +1,17 @@
import type { Directive } from 'vue'
import type { TestId } from './test-id'
function applyTestId(el: HTMLElement, value?: TestId | null) {
if (value) el.setAttribute('data-test-id', value)
else el.removeAttribute('data-test-id')
}
export const vTestId: Directive<HTMLElement, TestId | null | undefined> = {
mounted(el, binding) {
applyTestId(el, binding.value)
},
updated(el, binding) {
applyTestId(el, binding.value)
}
}

View file

@ -19,7 +19,7 @@ import {
import IconChevronRight from '~icons/lucide/chevron-right'
import { testId as testIdAttr, useI18n } from '@open-pencil/vue'
import { vTestId, useI18n } from '@open-pencil/vue'
import { useMenuUI } from '@/components/ui/menu'
import { IS_TAURI } from '@/constants'
import { useAppMenu } from '@/app/shell/menu/app-menu'
@ -91,7 +91,7 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' })
<MenubarRoot class="scrollbar-none flex items-center gap-0.5 overflow-x-auto">
<MenubarMenu v-for="menu in topMenus" :key="menu.label">
<MenubarTrigger
v-bind="testIdAttr(`menubar-${menu.label.toLowerCase()}`)"
v-test-id="`menubar-${menu.label.toLowerCase()}`"
class="flex cursor-pointer items-center rounded px-2 py-1 text-xs text-muted transition-colors select-none hover:bg-hover hover:text-surface data-[state=open]:bg-hover data-[state=open]:text-surface"
>
{{ menu.label }}

View file

@ -9,7 +9,7 @@ import {
ContextMenuPortal
} from 'reka-ui'
import {
testId as testIdAttr,
vTestId,
useEditorCommands,
useI18n,
useMenuModel,
@ -126,7 +126,7 @@ const contextCommandTestIds: Record<string, string> = {
</ContextMenuSub>
<ContextMenuItem
v-else
v-bind="testIdAttr(item.id ? contextCommandTestIds[item.id] : undefined)"
v-test-id="item.id ? contextCommandTestIds[item.id] : undefined"
:class="canvasMenuItemClass(item.label, cls)"
:disabled="item.disabled"
@select="item.action?.()"

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { inputNumberValue, testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
import { inputNumberValue, vTestId, type TestIdProps } from '@open-pencil/vue'
import { usePickerSliderUI } from './ui/picker-slider'
type PickerSliderDisplay = {
@ -64,7 +64,7 @@ function thumbLeft(): string {
</script>
<template>
<div :class="cls.root" v-bind="testIdAttr(testId)">
<div :class="cls.root" v-test-id="testId">
<span :class="cls.label">{{ label }}</span>
<div :class="cls.track">
<div :class="cls.gradient" :style="gradientStyle" />

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { testId as testIdAttr, type RequiredTestIdProps } from '@open-pencil/vue'
import { vTestId, type RequiredTestIdProps } from '@open-pencil/vue'
import type { Component } from 'vue'
interface ToolButtonProps extends RequiredTestIdProps {
@ -22,7 +22,7 @@ const emit = defineEmits<{
<template>
<button
v-bind="testIdAttr(testId)"
v-test-id="testId"
class="flex size-8 cursor-pointer items-center justify-center border-none transition-colors"
:class="[
mobile ? 'rounded-[6px] select-none' : 'rounded-lg',

View file

@ -11,7 +11,7 @@ import IconChevronDown from '~icons/lucide/chevron-down'
import { menu } from '@/components/ui/menu'
import ToolButton from '@/components/Toolbar/ToolButton.vue'
import { testId as testIdAttr, ToolbarItem } from '@open-pencil/vue'
import { vTestId, ToolbarItem } from '@open-pencil/vue'
import type { Tool } from '@open-pencil/vue'
import type { EditorToolDef } from '@open-pencil/core/editor'
@ -69,7 +69,7 @@ function activeKeyForTool() {
<DropdownMenuRoot>
<DropdownMenuTrigger as-child>
<button
v-bind="testIdAttr(`${mobile ? 'mobile-' : ''}toolbar-flyout-${tool.key.toLowerCase()}`)"
v-test-id="`${mobile ? 'mobile-' : ''}toolbar-flyout-${tool.key.toLowerCase()}`"
class="flex h-8 w-3 cursor-pointer items-center justify-center border-none transition-colors"
:class="[
mobile ? 'rounded-[6px] select-none' : 'rounded-lg',
@ -93,7 +93,7 @@ function activeKeyForTool() {
:tool="sub"
>
<DropdownMenuItem
v-bind="testIdAttr(`${mobile ? 'mobile-' : ''}toolbar-flyout-item-${sub.toLowerCase()}`)"
v-test-id="`${mobile ? 'mobile-' : ''}toolbar-flyout-item-${sub.toLowerCase()}`"
:class="menu().item({ class: subActive ? 'bg-accent text-white' : undefined })"
@select="actions.select"
>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { testId as testIdAttr } from '@open-pencil/vue'
import { vTestId } from '@open-pencil/vue'
import type { ToolbarActionItem } from '@/components/Toolbar/types'
@ -17,7 +17,7 @@ const emit = defineEmits<{
<button
v-for="item in actions"
:key="item.label"
v-bind="testIdAttr(`${testPrefix}-${item.label.toLowerCase()}`)"
v-test-id="`${testPrefix}-${item.label.toLowerCase()}`"
class="flex size-8 cursor-pointer items-center justify-center rounded-[6px] border-none bg-transparent text-muted transition-colors select-none active:bg-hover active:text-surface"
@click="emit('action', item)"
>

View file

@ -27,7 +27,7 @@ import {
} from 'reka-ui'
import { FlexRender } from '@tanstack/vue-table'
import { testId as testIdAttr, useI18n, useVariablesEditor } from '@open-pencil/vue'
import { vTestId, useI18n, useVariablesEditor } from '@open-pencil/vue'
import IconHash from '~icons/lucide/hash'
import IconPalette from '~icons/lucide/palette'
@ -390,7 +390,7 @@ function modeId(columnId: string): string {
v-for="item in variableTypes"
:key="item.type"
:class="menuCls.item"
v-bind="testIdAttr(`variables-add-${item.type.toLowerCase()}`)"
v-test-id="`variables-add-${item.type.toLowerCase()}`"
@select="ctx.addVariable(item.type)"
>
<component :is="variableTypeIcons[item.type]" :class="menuCls.icon" />

View file

@ -10,7 +10,7 @@ import {
AlertDialogTitle
} from 'reka-ui'
import { computed } from 'vue'
import { testId as testIdAttr } from '@open-pencil/vue'
import { vTestId } from '@open-pencil/vue'
import {
currentPermission,
@ -86,7 +86,7 @@ function handleDismiss() {
<AlertDialogAction
v-for="opt in allowOptions"
:key="opt.optionId"
v-bind="testIdAttr(`acp-permission-option-${opt.kind}`)"
v-test-id="`acp-permission-option-${opt.kind}`"
class="w-full rounded bg-accent px-3 py-1.5 text-xs font-medium text-white hover:bg-accent/90"
@click="respondToPermission(opt.optionId)"
>
@ -96,7 +96,7 @@ function handleDismiss() {
<AlertDialogCancel
v-for="opt in rejectOptions"
:key="opt.optionId"
v-bind="testIdAttr(`acp-permission-option-${opt.kind}`)"
v-test-id="`acp-permission-option-${opt.kind}`"
class="w-full rounded border border-border bg-canvas px-3 py-1.5 text-xs text-muted hover:bg-hover hover:text-surface"
@click="respondToPermission(opt.optionId)"
>

View file

@ -2,7 +2,7 @@
import { isTextUIPart, isToolUIPart, getToolName } from 'ai'
import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from 'reka-ui'
import { Markdown } from 'vue-stream-markdown'
import { testId as testIdAttr } from '@open-pencil/vue'
import { vTestId } from '@open-pencil/vue'
import 'vue-stream-markdown/index.css'
import type { UIDataTypes, UIMessage, UIMessagePart, UITools } from 'ai'
@ -41,7 +41,7 @@ function partKey(part: UIMessagePart<UIDataTypes, UITools>, index: number): stri
<template>
<div
v-bind="testIdAttr(`chat-message-${message.role}`)"
v-test-id="`chat-message-${message.role}`"
:class="message.role === 'user' ? 'flex justify-end' : ''"
>
<div class="min-w-0 space-y-1.5" :class="message.role === 'user' ? 'max-w-[85%]' : ''">

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
import { vTestId, type TestIdProps } from '@open-pencil/vue'
import Tip from '@/components/ui/Tip.vue'
@ -17,7 +17,7 @@ const emit = defineEmits<{
<template>
<Tip :label="label">
<button
v-bind="testIdAttr(testId)"
v-test-id="testId"
class="shrink-0 cursor-pointer border-none bg-transparent p-0 text-violet-400 hover:text-surface"
@click="emit('detach')"
>

View file

@ -4,7 +4,7 @@ import BoundVariableButton from '@/components/properties/BoundVariableButton.vue
import VariablePickerPopover from '@/components/properties/VariablePickerPopover.vue'
import { useIconButtonUI } from '@/components/ui/icon-button'
import { testId as testIdAttr, useI18n } from '@open-pencil/vue'
import { vTestId, useI18n } from '@open-pencil/vue'
import {
opacityFromPercent,
@ -99,7 +99,7 @@ const { panels, dialogs } = useI18n()
/>
<button
v-bind="testIdAttr(visibilityTestId)"
v-test-id="visibilityTestId"
:data-visible="item.visible ? 'true' : 'false'"
class="shrink-0 cursor-pointer border-none bg-transparent p-0 text-muted hover:text-surface"
@click="emit('toggleVisibility')"

View file

@ -4,7 +4,7 @@ import ColorInput from '@/components/ColorPicker/ColorInput.vue'
import ScrubInput from '@/components/ScrubInput.vue'
import { useIconButtonUI } from '@/components/ui/icon-button'
import { useSectionUI } from '@/components/ui/section'
import { PropertyListRoot, testId as testIdAttr, useEffectsControls, useI18n } from '@open-pencil/vue'
import { PropertyListRoot, vTestId, useEffectsControls, useI18n } from '@open-pencil/vue'
import { colorToCSS } from '@open-pencil/core/color'
@ -65,7 +65,7 @@ const sectionCls = useSectionUI()
/>
<button
v-bind="testIdAttr(`effect-visibility-${i}`)"
v-test-id="`effect-visibility-${i}`"
:data-visible="effect.visible ? 'true' : 'false'"
class="cursor-pointer border-none bg-transparent p-0 text-muted hover:text-surface"
@click="actions.toggleVisibility(i)"

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import Tip from '@/components/ui/Tip.vue'
import { testId as testIdAttr, useI18n, useLayoutControlsContext } from '@open-pencil/vue'
import { vTestId, useI18n, useLayoutControlsContext } from '@open-pencil/vue'
import type { LayoutMode } from '@open-pencil/core/scene-graph'
@ -42,7 +42,7 @@ const layoutModes: { mode: LayoutMode; test: string }[] = [
<button
v-for="dir in layoutModes"
:key="dir.mode"
v-bind="testIdAttr(`layout-direction-${dir.test}`)"
v-test-id="`layout-direction-${dir.test}`"
class="flex cursor-pointer items-center justify-center rounded border px-2 py-1"
:class="
(dir.mode === 'GRID' ? ctx.isGrid : ctx.node.layoutMode === dir.mode)

View file

@ -19,6 +19,7 @@ import VariablePickerPopover from '@/components/properties/VariablePickerPopover
import { useSelectUI } from '@/components/ui/select'
import {
testId as testIdAttr,
vTestId,
useI18n,
useLayoutControlsContext,
useNumberVariableBinding
@ -361,7 +362,7 @@ function handleSizeSelect(axis: 'width' | 'height', value: SizeSelectValue) {
@update:model-value="(value) => handleLimitSelect(item.prop, value as string)"
>
<SelectTrigger
v-bind="testIdAttr(`${item.testId}-menu`)"
v-test-id="`${item.testId}-menu`"
:reference="limitFieldAnchor(index)"
class="flex shrink-0 cursor-pointer items-center self-stretch border-none bg-transparent px-1 text-[11px] text-muted outline-none"
@pointerdown.stop
@ -403,7 +404,7 @@ function handleSizeSelect(axis: 'width' | 'height', value: SizeSelectValue) {
@update:model-value="(value) => handleLimitSelect(item.prop, value as string)"
>
<SelectTrigger
v-bind="testIdAttr(`${item.testId}-menu`)"
v-test-id="`${item.testId}-menu`"
:reference="limitFieldAnchor(index)"
class="flex shrink-0 cursor-pointer items-center self-stretch border-none bg-transparent px-1 text-[11px] text-muted outline-none"
@pointerdown.stop

View file

@ -12,7 +12,7 @@ import {
import { computed, nextTick, ref, watch } from 'vue'
import { testId as testIdAttr } from '@open-pencil/vue'
import { vTestId } from '@open-pencil/vue'
import type { Variable } from '@open-pencil/core/scene-graph'
@ -79,7 +79,7 @@ function submitCreate() {
<template>
<PopoverRoot v-model:open="open">
<PopoverTrigger
v-bind="testIdAttr(triggerTestId)"
v-test-id="triggerTestId"
:aria-label="triggerLabel"
:title="triggerLabel"
class="shrink-0 cursor-pointer border-none bg-transparent p-0 text-muted hover:text-surface"
@ -146,7 +146,7 @@ function submitCreate() {
class="min-w-0 flex-1 rounded border border-border bg-transparent px-1.5 py-1 text-[11px] text-surface outline-none placeholder:text-muted focus:border-accent"
/>
<button
v-bind="testIdAttr(createTestId)"
v-test-id="createTestId"
:disabled="!canCreate"
class="rounded border border-border bg-panel px-1.5 py-1 text-[11px] text-surface hover:bg-hover disabled:cursor-not-allowed disabled:opacity-50"
type="submit"
@ -156,7 +156,7 @@ function submitCreate() {
</form>
<button
v-else
v-bind="testIdAttr(createTestId)"
v-test-id="createTestId"
class="flex w-full cursor-pointer items-center gap-1.5 bg-transparent px-2 py-1.5 text-left text-[11px] text-muted hover:bg-hover hover:text-surface"
@click="startCreate"
>

View file

@ -11,7 +11,7 @@ import {
SelectTrigger,
SelectViewport
} from 'reka-ui'
import { testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
import { vTestId, type TestIdProps } from '@open-pencil/vue'
import { useSelectUI } from '@/components/ui/select'
@ -57,7 +57,7 @@ const separator = ui?.separator ?? 'mx-1 my-1 h-px bg-border'
<template>
<SelectRoot v-model="modelValue">
<SelectTrigger v-bind="testIdAttr(testId)" :class="select.trigger">
<SelectTrigger v-test-id="testId" :class="select.trigger">
<slot name="value">{{ displayValue }}</slot>
<icon-lucide-chevron-down class="size-2.5 shrink-0 text-muted" />
</SelectTrigger>

View file

@ -11,7 +11,7 @@ import {
SelectViewport
} from 'reka-ui'
import { testId as testIdAttr, type TestIdProps } from '@open-pencil/vue'
import { vTestId, type TestIdProps } from '@open-pencil/vue'
import { useSelectUI } from '@/components/ui/select'
import type { SelectUi } from '@/components/ui/select'
@ -47,7 +47,7 @@ const indicator = ui?.indicator ?? 'absolute left-1.5 inline-flex items-center j
<template>
<SelectRoot v-model="modelValue">
<SelectTrigger v-bind="testIdAttr(testId)" :class="select.trigger">
<SelectTrigger v-test-id="testId" :class="select.trigger">
<SelectValue :placeholder="placeholder" />
<icon-lucide-chevron-down class="ml-1 size-3 shrink-0 text-muted" />
</SelectTrigger>