Compare commits

...

6 Commits

Author SHA1 Message Date
GitHub Action
1f818ba529 [automated] Apply ESLint and Oxfmt fixes 2026-03-28 11:43:15 +00:00
bymyself
8680791f4a fix: remove unused emit declaration and eslint-disable directive 2026-03-28 04:40:10 -07:00
bymyself
c831562ec1 fix: suppress Vue ESLint rules for inline test component stubs 2026-03-28 03:39:29 -07:00
bymyself
35f79b7cd3 feat: add PrimeVue component stubs to test-utils (COMP-03) 2026-03-27 22:00:33 -07:00
GitHub Action
90a819bc87 [automated] Apply ESLint and Oxfmt fixes 2026-03-27 20:30:19 +00:00
bymyself
69239ca2d5 test: add standardized test-utils with Pinia + i18n defaults
Recreates src/utils/test-utils.ts (removed in #10471) as the canonical
shared render wrapper for VTL component tests.

Provides:
- renderWithDefaults(): auto-configures Pinia (stubActions: false),
  vue-i18n (English), and common directive stubs (tooltip)
- Optional userEvent instance (opt out via setupUser: false)
- Re-exports screen from @testing-library/vue

Eliminates duplicated createTestingPinia + createI18n boilerplate
across 97+ test files.

Part of: COMP-02 (standardize Pinia store test mocking patterns)
2026-03-27 13:27:06 -07:00
2 changed files with 286 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
/* eslint-disable vue/one-component-per-file, vue/no-reserved-component-names */
import { describe, expect, it, vi } from 'vitest'
import { render, screen, stubs } from '@/utils/test-utils'
import { defineComponent, h } from 'vue'
const TestButton = defineComponent({
props: { label: { type: String, required: true } },
setup(props) {
return () => h('button', { 'data-testid': 'test-btn' }, props.label)
}
})
describe('test-utils', () => {
it('renders a component with default plugins', () => {
render(TestButton, { props: { label: 'Click me' } })
expect(screen.getByTestId('test-btn')).toHaveTextContent('Click me')
})
it('provides a userEvent instance by default', () => {
const { user } = render(TestButton, { props: { label: 'Click' } })
expect(user).toBeDefined()
})
it('allows opting out of userEvent', () => {
const { user } = render(TestButton, {
props: { label: 'Click' },
setupUser: false
})
expect(user).toBeUndefined()
})
})
describe('stubs', () => {
describe('Button', () => {
it('renders as a button element with label', () => {
const Wrapper = defineComponent({
components: { Button: stubs.Button },
setup() {
return () => h(stubs.Button, { label: 'Save' })
}
})
render(Wrapper)
expect(screen.getByRole('button')).toHaveTextContent('Save')
})
it('sets disabled when loading is true', () => {
const Wrapper = defineComponent({
setup() {
return () => h(stubs.Button, { label: 'Save', loading: true })
}
})
render(Wrapper)
expect(screen.getByRole('button')).toBeDisabled()
})
it('emits click event', async () => {
const onClick = vi.fn()
const Wrapper = defineComponent({
setup() {
return () => h(stubs.Button, { label: 'Go', onClick })
}
})
const { user } = render(Wrapper)
await user!.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalled()
})
})
describe('Skeleton', () => {
it('renders with data-testid', () => {
const Wrapper = defineComponent({
setup() {
return () => h(stubs.Skeleton)
}
})
render(Wrapper)
expect(screen.getByTestId('skeleton')).toBeDefined()
})
})
describe('Dialog', () => {
it('renders children when visible', () => {
const Wrapper = defineComponent({
setup() {
return () =>
h(stubs.Dialog, { visible: true }, () => h('p', 'Dialog body'))
}
})
render(Wrapper)
expect(screen.getByRole('dialog')).toHaveTextContent('Dialog body')
})
it('renders nothing when not visible', () => {
const Wrapper = defineComponent({
setup() {
return () =>
h(stubs.Dialog, { visible: false }, () => h('p', 'Hidden'))
}
})
render(Wrapper)
expect(screen.queryByRole('dialog')).toBeNull()
})
})
})

180
src/utils/test-utils.ts Normal file
View File

@@ -0,0 +1,180 @@
/* eslint-disable vue/one-component-per-file, vue/require-prop-types, vue/no-reserved-component-names */
import type { RenderResult } from '@testing-library/vue'
import type { ComponentMountingOptions } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { defineComponent, h } from 'vue'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json'
/**
* Creates the default set of Vue plugins for component tests.
*
* - Pinia with `stubActions: false` (actions execute, but are spied)
* - vue-i18n with English locale
*
* Pass additional plugins via the `plugins` option in `renderWithDefaults`.
*/
function createDefaultPlugins() {
return [
createTestingPinia({ stubActions: false }),
createI18n({
legacy: false,
locale: 'en',
messages: { en: enMessages }
})
]
}
/**
* Common directive stubs for components that use PrimeVue/custom directives.
* Prevents "Failed to resolve directive" warnings in test output.
*/
const defaultDirectiveStubs: Record<string, () => void> = {
tooltip: () => {}
}
/**
* PrimeVue component stubs for unit/component tests.
*
* Use via `global.stubs` in render options:
* ```ts
* render(MyComponent, { global: { stubs: { Button: stubs.Button } } })
* ```
*
* Or use `renderWithDefaults` which auto-applies these as defaults.
*/
const ButtonStub = defineComponent({
name: 'Button',
props: [
'disabled',
'loading',
'variant',
'size',
'label',
'icon',
'severity'
],
emits: ['click'],
setup(props, { slots, emit }) {
return () =>
h(
'button',
{
disabled: props.disabled || props.loading,
'data-testid': props.label,
'data-icon': props.icon,
onClick: () => emit('click')
},
slots.default?.() ?? props.label
)
}
})
const SkeletonStub = defineComponent({
name: 'Skeleton',
setup() {
return () => h('div', { 'data-testid': 'skeleton' })
}
})
const TagStub = defineComponent({
name: 'Tag',
props: ['value', 'severity'],
setup(props, { slots }) {
return () =>
h('span', { 'data-testid': 'tag' }, slots.default?.() ?? props.value)
}
})
const BadgeStub = defineComponent({
name: 'Badge',
props: ['value', 'severity'],
setup(props) {
return () => h('span', { 'data-testid': 'badge' }, props.value)
}
})
const MessageStub = defineComponent({
name: 'Message',
props: ['severity', 'closable'],
setup(_, { slots }) {
return () =>
h('div', { 'data-testid': 'message', role: 'alert' }, slots.default?.())
}
})
const DialogStub = defineComponent({
name: 'Dialog',
props: ['visible', 'modal', 'header'],
setup(props, { slots }) {
return () =>
props.visible
? h('div', { role: 'dialog', 'data-testid': 'dialog' }, [
props.header ? h('div', props.header) : null,
slots.default?.()
])
: null
}
})
const stubs = {
Button: ButtonStub,
Skeleton: SkeletonStub,
Tag: TagStub,
Badge: BadgeStub,
Message: MessageStub,
Dialog: DialogStub
} as const
type RenderWithDefaultsResult = RenderResult & {
user: ReturnType<typeof userEvent.setup> | undefined
}
/**
* Renders a Vue component with standard test infrastructure pre-configured:
* - Pinia testing store (actions execute but are spied)
* - vue-i18n with English messages
* - Common directive stubs (tooltip)
* - Optional userEvent instance
*
* @example
* ```ts
* import { render, screen } from '@/utils/test-utils'
*
* it('renders button text', async () => {
* const { user } = render(MyComponent, { props: { label: 'Click' } })
* expect(screen.getByRole('button')).toHaveTextContent('Click')
* await user!.click(screen.getByRole('button'))
* })
* ```
*/
function renderWithDefaults<C>(
component: C,
options?: ComponentMountingOptions<C> & { setupUser?: boolean }
): RenderWithDefaultsResult {
const { setupUser = true, global: globalOptions, ...rest } = options ?? {}
const user = setupUser ? userEvent.setup() : undefined
const result = render(
component as Parameters<typeof render>[0],
{
global: {
...globalOptions,
plugins: [...createDefaultPlugins(), ...(globalOptions?.plugins ?? [])],
directives: {
...defaultDirectiveStubs,
...globalOptions?.directives
}
},
...rest
} as Parameters<typeof render>[1]
)
return { ...result, user }
}
export { renderWithDefaults as render, screen, stubs }