Skip to content

Commit

Permalink
feat: Add isAutoSnapshot prop to customizeSnapshotId()
Browse files Browse the repository at this point in the history
fix: workaround `preview.beforeEach` order issue
  • Loading branch information
unional committed Mar 3, 2025
1 parent 15bbbed commit 4692f23
Show file tree
Hide file tree
Showing 21 changed files with 127 additions and 51 deletions.
6 changes: 6 additions & 0 deletions .changeset/better-bikes-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"storybook-addon-vis": patch
---

Workaround a Storybook 8.4 issue where the `preview.beforeEach` is called after test instead of before.
Because of that, the `setAutoSnapshotOptions()` called within test is not honored.
5 changes: 5 additions & 0 deletions .changeset/olive-islands-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vitest-plugin-vis": minor
---

Add `isAutoSnapshot` prop to `customizeSnapshotId()`.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { screen } from '@testing-library/react'
import { page } from '@vitest/browser/context'
import { expect, it } from 'vitest'
import { render } from 'vitest-browser-react'
import { hasImageSnapshot } from '../../index.ts'
import { hasImageSnapshot, setAutoSnapshotOptions } from '../../index.ts'
import { UNI_PNG_BASE64 } from '../../testing.ts'
import * as stories from './to_match_image_snapshot.stories.tsx'

Expand Down Expand Up @@ -61,6 +61,26 @@ it('can customize snapshot filename', async () => {
).toBeTruthy()
})

it.sequential('can customize auto snapshot filename', async () => {
setAutoSnapshotOptions({
enable: true,
customizeSnapshotId({ id, isAutoSnapshot }) {
return isAutoSnapshot ? `${id}-at` : `${id}-invalid`
},
})

await MatchingElement.run()
})

it.sequential('can customize auto snapshot filename (validate)', async () => {
setAutoSnapshotOptions(false)
expect(
await hasImageSnapshot({
customizeSnapshotId: ({ id }) => `${id.slice(0, -' (validate)'.length)}-at`,
}),
).toBeTruthy()
})

it('can use screen from @testing-library/react to get element', async () => {
await MatchingElement.run()
const subject = screen.getByTestId('subject')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ export function hasImageSnapshot(options?: ImageSnapshotIdOptions | undefined) {
if (options?.customizeSnapshotId) {
return commands
.imageSnapshotNextIndex(taskId)
.then((index) => commands.hasImageSnapshot(taskId, options.customizeSnapshotId!({ id: taskId, index })))
.then((index) =>
commands.hasImageSnapshot(
taskId,
options.customizeSnapshotId!({ id: taskId, index, isAutoSnapshot: !!test.meta.vis?.isAutoSnapshot }),
),
)
}

return commands.hasImageSnapshot(taskId)
Expand Down
3 changes: 2 additions & 1 deletion packages/storybook-addon-vis/src/client/vitest_proxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { BrowserCommands } from '@vitest/browser/context'
import type { CurrentTest } from 'vitest-plugin-vis/client'
import type {
HasImageSnapshotCommand,
ImageSnapshotNextIndexCommand,
Expand Down Expand Up @@ -36,4 +37,4 @@ export const commands = new Proxy<BrowserCommands>({} as any, {
},
})

export const getCurrentTest = () => vitestSuite?.getCurrentTest()
export const getCurrentTest = () => vitestSuite?.getCurrentTest() as CurrentTest
10 changes: 9 additions & 1 deletion packages/storybook-addon-vis/src/preview/vis_annotation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import type { ProjectAnnotations, Renderer, StoryContext } from 'storybook/internal/types'
import { setAutoSnapshotOptions } from 'vitest-plugin-vis'
import { isSnapshotEnabled } from '../client/storybook/param.ts'
import { getCurrentTest } from '../client/vitest_proxy.ts'

export const visAnnotations = {
beforeEach(ctx: StoryContext) {
setAutoSnapshotOptions({ enable: isSnapshotEnabled(ctx.tags), ...ctx.parameters?.snapshot, tags: ctx.tags })
// console.debug('storbook-addon-vis.preview.beforeEach starts...')
const test = getCurrentTest()
setAutoSnapshotOptions({
enable: isSnapshotEnabled(ctx.tags),
...ctx.parameters?.snapshot,
tags: ctx.tags,
...test.meta.vis,
})
},
} satisfies ProjectAnnotations<Renderer>
2 changes: 1 addition & 1 deletion packages/vitest-plugin-vis/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ export * from './client/expect/expectation_result.ts'
export type * from './client/expect/to_match_image_snapshot.types.ts'
export * from './client/image_snapshot_matcher.ts'
export * from './client/should_take_snapshot.ts'
export { type MetaTask, type SnapshotMeta, setAutoSnapshotOptions } from './client/snapshot_options.ts'
export { setAutoSnapshotOptions, type MetaTask } from './client/snapshot_options.ts'
export * from './client/task_id.ts'
export type * from './shared/types.ts'
4 changes: 2 additions & 2 deletions packages/vitest-plugin-vis/src/client/ctx.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getCurrentSuite, getCurrentTest } from './vitest_suite_proxy.ts'

export const ctx = {
autoEnabled: false,
autoEnabled: undefined as boolean | undefined,
getCurrentTest,
getCurrentSuite,
__test__reset() {
ctx.getCurrentTest = getCurrentTest
ctx.getCurrentSuite = getCurrentSuite
ctx.autoEnabled = false
ctx.autoEnabled = undefined
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,23 @@ describe(`${setAutoSnapshotOptions.name}()`, () => {

// can't validate 2nd snapshot because it's chicken-egg problem
})

it.sequential('can customize auto snapshot filename', async () => {
setAutoSnapshotOptions({
customizeSnapshotId({ id, isAutoSnapshot }) {
return isAutoSnapshot ? `${id}-at` : `${id}-invalid`
},
})
page.render(<div>hello</div>)
})

it.sequential('can customize auto snapshot filename (validate)', async ({ expect }) => {
expect(
await page.hasImageSnapshot({
customizeSnapshotId: ({ id }) => `${id.slice(0, -' (validate)'.length)}-at`,
}),
).toBeTruthy()
})
})

describe('ssim', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { commands } from '@vitest/browser/context'
import type { AsyncExpectationResult } from '@vitest/expect'
import type { ToMatchImageSnapshotOptions } from '../../shared/types.ts'
import { ctx } from '../ctx.ts'
import { imageSnapshotMatcher } from '../image_snapshot_matcher.ts'
import { success } from './expectation_result.ts'
import type { ToMatchImageSnapshotOptions } from './to_match_image_snapshot.types.ts'

export function toMatchImageSnapshot(
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
import type {
ComparisonMethod,
ImageSnapshotCompareOptions,
ImageSnapshotIdOptions,
ImageSnapshotTimeoutOptions,
} from '../../shared/types.ts'
import type { ComparisonMethod, ToMatchImageSnapshotOptions } from '../../shared/types.ts'

export interface ImageSnapshotMatcher {
toMatchImageSnapshot<M extends ComparisonMethod>(options?: ToMatchImageSnapshotOptions<M> | undefined): Promise<void>
}

export type ToMatchImageSnapshotOptions<M extends ComparisonMethod = 'pixel'> = ImageSnapshotTimeoutOptions &
ImageSnapshotIdOptions &
ImageSnapshotCompareOptions<M> & {
/**
* Expect the matcher to fail.
* If it passes, it will throw an error with details.
*/
expectToFail?: boolean | undefined
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'
import type { ToMatchImageSnapshotOptions } from '../shared/types.ts'

export function prettifyOptions(options: ToMatchImageSnapshotOptions<any> | undefined) {
if (!options) return 'none'
Expand Down
17 changes: 12 additions & 5 deletions packages/vitest-plugin-vis/src/client/image_snapshot_matcher.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import dedent from 'dedent'
import { resolve } from 'pathe'
import type { Task } from 'vitest'
import type { ImageSnapshotNextIndexCommand } from '../commands.ts'
import type { PrepareImageSnapshotComparisonCommand } from '../server/commands/prepare_image_snapshot_comparison.ts'
import type { WriteImageSnapshotCommand } from '../server/commands/write_image_snapshot.ts'
import { isBase64String } from '../shared/base64.ts'
import { compareImage } from '../shared/compare_image.ts'
import type { CurrentTest, ToMatchImageSnapshotOptions } from '../shared/types.ts'
import { alignImagesToSameSize } from './align_images.ts'
import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'
import { toDataURL, toImageData } from './image_data.ts'
import { prettifyOptions } from './image_snapshot_matcher.logic.ts'
import { convertElementToCssSelector } from './selector.ts'
Expand All @@ -17,12 +16,19 @@ import { server } from './vitest_browser_context_proxy.ts'
export function imageSnapshotMatcher(
commands: PrepareImageSnapshotComparisonCommand & WriteImageSnapshotCommand & ImageSnapshotNextIndexCommand,
) {
return async function matchImageSnapshot(test: Task, subject: any, options?: ToMatchImageSnapshotOptions<any>) {
return async function matchImageSnapshot(
test: CurrentTest & {},
subject: any,
options?: ToMatchImageSnapshotOptions<any>,
) {
const isAutoSnapshot = !!test.meta.vis?.isAutoSnapshot
const taskId = toTaskId(test)
const info = await commands.prepareImageSnapshotComparison(
taskId,
parseImageSnapshotSubject(subject),
options?.customizeSnapshotId ? await parseImageSnapshotOptions(commands, taskId, options) : options,
options?.customizeSnapshotId
? await parseImageSnapshotOptions(commands, taskId, isAutoSnapshot, options)
: options,
)

if (!info) return
Expand Down Expand Up @@ -103,10 +109,11 @@ async function writeSnapshot(commands: WriteImageSnapshotCommand, path: string,
async function parseImageSnapshotOptions(
commands: ImageSnapshotNextIndexCommand,
taskId: string,
isAutoSnapshot: boolean,
options: ToMatchImageSnapshotOptions<any>,
) {
const index = await commands.imageSnapshotNextIndex(taskId)
const { customizeSnapshotId, ...rest } = options
const snapshotFileId = customizeSnapshotId!({ id: taskId, index })
const snapshotFileId = customizeSnapshotId!({ id: taskId, index, isAutoSnapshot })
return { ...rest, snapshotFileId }
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export function hasImageSnapshot(this: BrowserPage, options?: ImageSnapshotIdOpt
if (options?.customizeSnapshotId) {
return commands
.imageSnapshotNextIndex(taskId)
.then((index) => commands.hasImageSnapshot(taskId, options.customizeSnapshotId!({ id: taskId, index })))
.then((index) =>
commands.hasImageSnapshot(
taskId,
options.customizeSnapshotId!({ id: taskId, index, isAutoSnapshot: !!test.meta.vis?.isAutoSnapshot }),
),
)
}

return commands.hasImageSnapshot(taskId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SnapshotMeta } from '../shared/types.ts'
import { ctx } from './ctx.ts'
import { type SnapshotMeta, extractAutoSnapshotOptions } from './snapshot_options.ts'
import { extractAutoSnapshotOptions } from './snapshot_options.ts'

/**
* Determine should snapshot be taken.
Expand Down
9 changes: 1 addition & 8 deletions packages/vitest-plugin-vis/src/client/snapshot_options.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { NAME } from '../shared/constants.ts'
import type { AutoSnapshotOptions, ComparisonMethod } from '../shared/types.ts'
import type { ComparisonMethod, SnapshotMeta } from '../shared/types.ts'
import { ctx } from './ctx.ts'
import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'

export type SnapshotMeta<M extends ComparisonMethod> = ToMatchImageSnapshotOptions<M> &
AutoSnapshotOptions & {
enable?: boolean | undefined
[key: string]: unknown
}

type Suite = { meta: Record<string, any>; suite?: Suite | undefined }

Expand Down
8 changes: 2 additions & 6 deletions packages/vitest-plugin-vis/src/client/vitest_suite_proxy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { NAME } from '../shared/constants.ts'
import type { SnapshotMeta } from './snapshot_options.ts'
import type { CurrentTest } from '../shared/types.ts'

let vitestSuite: Awaited<typeof import('vitest/suite')>

Expand All @@ -9,8 +8,5 @@ if ((globalThis as any).__vitest_browser__) {
})
}

export const getCurrentTest = () =>
vitestSuite?.getCurrentTest() as ReturnType<typeof vitestSuite.getCurrentTest> & {
meta: { [NAME]?: SnapshotMeta<'pixel' | 'ssim'> }
}
export const getCurrentTest = () => vitestSuite?.getCurrentTest() as CurrentTest
export const getCurrentSuite = () => vitestSuite?.getCurrentSuite()
3 changes: 2 additions & 1 deletion packages/vitest-plugin-vis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
import './client/expect/augment.ts'
import './client/page/augment.ts'

export { type MetaTask, type SnapshotMeta, setAutoSnapshotOptions } from './client/snapshot_options.ts'
export { setAutoSnapshotOptions, type MetaTask } from './client/snapshot_options.ts'
export type * from './shared/types.ts'
6 changes: 5 additions & 1 deletion packages/vitest-plugin-vis/src/server/vis_context.logic.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { join, relative } from 'pathe'
import { pick } from 'type-plus'
import { getCurrentTest } from 'vitest/suite'
import type { VisOptions } from '../config/types.ts'
import { BASELINE_DIR, DIFF_DIR, RESULT_DIR } from '../shared/constants.ts'
import type { CurrentTest } from '../shared/types.ts'
import { file } from './file.ts'
import { getSnapshotSubpath, resolveSnapshotRootDir } from './snapshot_path.ts'
import { ctx } from './vis_context.ctx.ts'
Expand Down Expand Up @@ -70,11 +72,13 @@ export function createVisContext() {
},
getSnapshotFilename(info: { taskId: string; task: { count: number } }, snapshotFileId: string | undefined) {
if (snapshotFileId) return `${snapshotFileId}.png`

const test = getCurrentTest() as CurrentTest
const isAutoSnapshot = !!test?.meta.vis?.isAutoSnapshot
const customizeSnapshotId = visOptions.customizeSnapshotId ?? (({ id, index }) => `${id}-${index}`)
return `${customizeSnapshotId({
id: info.taskId,
index: info.task.count,
isAutoSnapshot,
})}.png`
},
getSuiteInfo(testPath: string, taskId: string) {
Expand Down
4 changes: 3 additions & 1 deletion packages/vitest-plugin-vis/src/setup/create_vis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export function createVis<SM extends SnapshotMeta<ComparisonMethod>>(commands: S
const meta = extractAutoSnapshotOptions(test)
if (!shouldTakeSnapshot(meta)) return

test.meta.vis = { ...test.meta.vis, isAutoSnapshot: true }

await test!.context.expect(getSubject(meta?.subjectDataTestId ?? subjectDataTestId)).toMatchImageSnapshot(meta)
},
matchPerTheme(themes) {
Expand All @@ -114,7 +116,7 @@ export function createVis<SM extends SnapshotMeta<ComparisonMethod>>(commands: S
.toMatchImageSnapshot({
...meta,
customizeSnapshotId: meta?.customizeSnapshotId
? ({ id, index }) => `${meta.customizeSnapshotId!({ id, index })}-${themeId}`
? ({ id, index }) => `${meta.customizeSnapshotId!({ id, index, isAutoSnapshot: true })}-${themeId}`
: ({ id }) => `${id}-${themeId}`,
})
} catch (error) {
Expand Down
24 changes: 22 additions & 2 deletions packages/vitest-plugin-vis/src/shared/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type pixelMatch from 'pixelmatch'
import type { Options as SsimOptions } from 'ssim.js'
import type { getCurrentTest } from 'vitest/suite'
import type { NAME } from './constants.ts'

export interface ImageSnapshotTimeoutOptions {
/**
Expand All @@ -19,9 +21,8 @@ export interface ImageSnapshotIdOptions {
*/
customizeSnapshotId?: (context: {
id: string
// width: number
// height: number
index: number
isAutoSnapshot: boolean
}) => string
}

Expand Down Expand Up @@ -71,3 +72,22 @@ export type AutoSnapshotOptions = {
*/
subjectDataTestId?: string | undefined
}
export type ToMatchImageSnapshotOptions<M extends ComparisonMethod = 'pixel'> = ImageSnapshotTimeoutOptions &
ImageSnapshotIdOptions &
ImageSnapshotCompareOptions<M> & {
/**
* Expect the matcher to fail.
* If it passes, it will throw an error with details.
*/
expectToFail?: boolean | undefined
}

export type SnapshotMeta<M extends ComparisonMethod> = ToMatchImageSnapshotOptions<M> &
AutoSnapshotOptions & {
enable?: boolean | undefined
[key: string]: unknown
}

export type CurrentTest = ReturnType<typeof getCurrentTest> & {
meta: { [NAME]?: SnapshotMeta<'pixel' | 'ssim'> & { isAutoSnapshot?: boolean | undefined } }
}

0 comments on commit 4692f23

Please sign in to comment.