Last active
October 14, 2024 15:10
-
-
Save aurbano/abf0e65ee6ff008fe75b88914c2aed48 to your computer and use it in GitHub Desktop.
Storybook Snapshot Testing (HTML & visual)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { configureToMatchImageSnapshot } from 'jest-image-snapshot'; | |
import { close, start } from 'jsdom-screenshot-playwright'; | |
import { afterAll, beforeAll, expect } from 'vitest'; | |
import { render } from '@testing-library/react'; | |
// Update this to match the path to your preview file | |
import * as previewAnnotations from '.storybook/preview'; | |
// Customise jest-image-snapshot options | |
const toMatchImageSnapshot = configureToMatchImageSnapshot({ | |
diffDirection: 'vertical', | |
// useful on CI (no need to retrieve the diff image, copy/paste image content from logs) | |
dumpDiffToConsole: false, | |
// use SSIM to limit false positive | |
// https://github.com/americanexpress/jest-image-snapshot#recommendations-when-using-ssim-comparison | |
comparisonMethod: 'ssim', | |
customDiffConfig: { | |
ssim: 'fast', | |
}, | |
failureThreshold: 0.01, | |
failureThresholdType: 'percent', | |
}); | |
expect.extend({ | |
toMatchImageSnapshot, | |
}); | |
if (!process.env.CI) { | |
// We only do the Playwright snapshots outside of CI | |
beforeAll(async () => { | |
// start jsdom-screenshot-playwright before all tests (to avoid starting it for each test and improve performance) | |
await start( | |
{ | |
defaultSelector: 'body', // first div element in rendered html | |
device: 'Desktop Firefox HiDPI', | |
}, | |
{ | |
// playwright context options | |
viewport: { | |
width: 800, | |
height: 600, | |
}, | |
}, | |
); | |
}); | |
afterAll(async () => { | |
// close jsdom-screenshot-playwright after all tests (close playwright instance) | |
await close(); | |
}); | |
} | |
function setupStorybookTestEnvironment() { | |
// Storybook configuration | |
setProjectAnnotations([previewAnnotations, { testingLibraryRender: render }]); | |
// Mock the ResizeObserver | |
const ResizeObserverMock = vi.fn(() => ({ | |
observe: vi.fn(), | |
unobserve: vi.fn(), | |
disconnect: vi.fn(), | |
})); | |
// Stub the global ResizeObserver | |
vi.stubGlobal('ResizeObserver', ResizeObserverMock); | |
}; | |
setupStorybookTestEnvironment(); | |
export * from '@testing-library/react'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { composeStories, type Meta, type StoryFn } from '@storybook/react'; | |
import { generateImage } from 'jsdom-screenshot-playwright'; | |
import path from 'path'; | |
import { describe, expect, test } from 'vitest'; | |
type StoryFile = { | |
default: Meta; | |
[name: string]: StoryFn | Meta; | |
}; | |
describe('Storybook Tests', async () => { | |
const storyFiles = getAllStoryFiles(); | |
for (const { storyFile: storyFileFn, componentName } of storyFiles) { | |
patchMatchMedia(); | |
const storyFile = await storyFileFn(); | |
const meta = storyFile.default; | |
const title = meta.title || componentName; | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | |
if (meta.parameters?.storyshots?.disable) { | |
// Skip component tests if they are disabled | |
continue; | |
} | |
describe(title, async () => { | |
const stories = Object.entries(compose(storyFile)).map(([name, story]) => ({ name, story })); | |
if (stories.length <= 0) { | |
throw new Error(` | |
No stories found for this module: ${title}. | |
Make sure there is at least one valid story for this module, | |
without a disable parameter, or add parameters.storyshots.disable in the default export of this file. | |
`); | |
} | |
stories.forEach(({ name, story }) => | |
test(name, async () => { | |
await story.run(); | |
// await story.play?.(story); | |
// Ensures a consistent snapshot by waiting for the component to render by adding a delay | |
// before taking the snapshot. | |
// eslint-disable-next-line no-promise-executor-return | |
await new Promise((resolve) => setTimeout(resolve, 1)); | |
expect(document.body.firstChild).toMatchSnapshot(); | |
if (!process.env.CI) { | |
// Only run image snapshot tests locally | |
expect(await generateImage()).toMatchImageSnapshot(); | |
} | |
}), | |
); | |
}); | |
} | |
}); | |
const compose = (entry: StoryFile): ReturnType<typeof composeStories<StoryFile>> => { | |
try { | |
return composeStories(entry); | |
} catch (e) { | |
throw new Error( | |
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions | |
`There was an issue composing stories for the module: ${JSON.stringify(entry)}, ${e}`, | |
); | |
} | |
}; | |
function getAllStoryFiles() { | |
const storyFiles = [ | |
...Object.entries( | |
import.meta.glob<StoryFile>(`../components/app/**/*.(stories|story).@(js|jsx|mjs|ts|tsx)`), | |
), | |
...Object.entries( | |
import.meta.glob<StoryFile>(`../pages/**/*.(stories|story).@(js|jsx|mjs|ts|tsx)`), | |
), | |
]; | |
return storyFiles.map(([filePath, storyFile]) => { | |
const storyDir = path.dirname(filePath); | |
const componentName = path.basename(filePath).replace(/\.(?:stories|story)\.[^/.]+$/, ''); | |
return { filePath, storyFile, componentName, storyDir }; | |
}); | |
} | |
function patchMatchMedia() { | |
// eslint-disable-next-line functional/immutable-data | |
Object.defineProperty(window, 'matchMedia', { | |
writable: true, | |
value: vi.fn((query) => ({ | |
matches: false, | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | |
media: query, | |
onchange: null, | |
addListener: vi.fn(), // Deprecated | |
removeListener: vi.fn(), // Deprecated | |
addEventListener: vi.fn(), | |
removeEventListener: vi.fn(), | |
dispatchEvent: vi.fn(), | |
})), | |
}); | |
// eslint-disable-next-line functional/immutable-data | |
Element.prototype.scrollIntoView = vi.fn(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment