2023-01-27 15:51:35 +01:00
|
|
|
import { toMatchImageSnapshot } from 'jest-image-snapshot'
|
2023-02-03 13:06:21 +01:00
|
|
|
import { OptionsParameter } from '@storybook/addons'
|
2023-01-27 15:51:35 +01:00
|
|
|
import { getStoryContext, TestRunnerConfig, TestContext } from '@storybook/test-runner'
|
2023-02-03 13:06:21 +01:00
|
|
|
import type { Locator, Page, LocatorScreenshotOptions } from 'playwright-core'
|
|
|
|
import type { Mocks } from '~/mocks/utils'
|
2023-02-10 12:50:43 +01:00
|
|
|
import { StoryContext } from '@storybook/react'
|
|
|
|
|
|
|
|
type SupportedBrowserName = 'chromium' | 'firefox' | 'webkit'
|
2023-02-03 13:06:21 +01:00
|
|
|
|
|
|
|
// Extend Storybook interface `Parameters` with Chromatic parameters
|
|
|
|
declare module '@storybook/react' {
|
|
|
|
interface Parameters {
|
|
|
|
options?: OptionsParameter
|
|
|
|
layout?: 'padded' | 'fullscreen' | 'centered'
|
|
|
|
testOptions?: {
|
2023-02-24 20:33:39 +01:00
|
|
|
/**
|
|
|
|
* Whether the test should be a no-op (doesn't jest.skip as @storybook/test-runner doesn't allow that).
|
|
|
|
* @default false
|
|
|
|
*/
|
2023-02-03 13:06:21 +01:00
|
|
|
skip?: boolean
|
2023-02-24 20:33:39 +01:00
|
|
|
/**
|
2023-03-02 22:11:03 +01:00
|
|
|
* Whether we should wait for all loading indicators to disappear before taking a snapshot.
|
|
|
|
*
|
|
|
|
* This is on by default for stories that have a layout of 'fullscreen', and off otherwise.
|
|
|
|
* Override that behavior by setting this to `true` or `false` manually.
|
|
|
|
*
|
|
|
|
* You can also provide a selector string instead of a boolean - in that case we'll wait
|
|
|
|
* for a matching element to be be visible once all loaders are gone.
|
2023-02-24 20:33:39 +01:00
|
|
|
*/
|
2023-03-02 22:11:03 +01:00
|
|
|
waitForLoadersToDisappear?: boolean | string
|
2023-02-03 13:06:21 +01:00
|
|
|
/**
|
|
|
|
* Whether navigation (sidebar + topbar) should be excluded from the snapshot.
|
|
|
|
* Warning: Fails if enabled for stories in which navigation is not present.
|
|
|
|
*/
|
|
|
|
excludeNavigationFromSnapshot?: boolean
|
2023-02-10 12:50:43 +01:00
|
|
|
/**
|
|
|
|
* The test will always run for all the browers, but snapshots are only taken in Chromium by default.
|
|
|
|
* Override this to take snapshots in other browsers too.
|
2023-02-24 20:33:39 +01:00
|
|
|
* @default ['chromium']
|
2023-02-10 12:50:43 +01:00
|
|
|
*/
|
|
|
|
snapshotBrowsers?: SupportedBrowserName[]
|
2023-02-03 13:06:21 +01:00
|
|
|
}
|
|
|
|
mockDate?: string | number | Date
|
|
|
|
msw?: {
|
|
|
|
mocks?: Mocks
|
|
|
|
}
|
|
|
|
[name: string]: any
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-10 12:50:43 +01:00
|
|
|
const RETRY_TIMES = 5
|
2023-02-24 20:33:39 +01:00
|
|
|
const LOADER_SELECTORS = ['.ant-skeleton', '.Spinner', '.LemonSkeleton', '.LemonTableLoader']
|
2023-01-27 15:51:35 +01:00
|
|
|
|
|
|
|
const customSnapshotsDir = `${process.cwd()}/frontend/__snapshots__`
|
|
|
|
|
2023-02-10 12:50:43 +01:00
|
|
|
module.exports = {
|
|
|
|
setup() {
|
|
|
|
expect.extend({ toMatchImageSnapshot })
|
|
|
|
jest.retryTimes(RETRY_TIMES, { logErrorsBeforeRetry: true })
|
|
|
|
},
|
|
|
|
async postRender(page, context) {
|
2023-03-02 22:11:03 +01:00
|
|
|
const browserContext = page.context()
|
2023-02-10 12:50:43 +01:00
|
|
|
const storyContext = await getStoryContext(page, context)
|
2023-02-24 20:33:39 +01:00
|
|
|
const { skip = false, snapshotBrowsers = ['chromium'] } = storyContext.parameters?.testOptions ?? {}
|
2023-02-10 12:50:43 +01:00
|
|
|
|
2023-03-02 22:11:03 +01:00
|
|
|
browserContext.setDefaultTimeout(1000) // Reduce the default timeout from 30 s to 1 s to pre-empt Jest timeouts
|
2023-02-24 20:33:39 +01:00
|
|
|
if (!skip) {
|
2023-03-02 22:11:03 +01:00
|
|
|
const currentBrowser = browserContext.browser()!.browserType().name() as 'chromium' | 'firefox' | 'webkit'
|
2023-02-10 12:50:43 +01:00
|
|
|
if (snapshotBrowsers.includes(currentBrowser)) {
|
|
|
|
await expectStoryToMatchSnapshot(page, context, storyContext, currentBrowser)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
} as TestRunnerConfig
|
|
|
|
|
|
|
|
async function expectStoryToMatchSnapshot(
|
|
|
|
page: Page,
|
|
|
|
context: TestContext,
|
|
|
|
storyContext: StoryContext,
|
|
|
|
browser: SupportedBrowserName
|
|
|
|
): Promise<void> {
|
2023-03-02 22:11:03 +01:00
|
|
|
const {
|
|
|
|
waitForLoadersToDisappear = storyContext.parameters?.layout === 'fullscreen',
|
|
|
|
excludeNavigationFromSnapshot = false,
|
|
|
|
} = storyContext.parameters?.testOptions ?? {}
|
2023-02-24 20:33:39 +01:00
|
|
|
|
2023-02-10 12:50:43 +01:00
|
|
|
let check: (page: Page, context: TestContext, browser: SupportedBrowserName) => Promise<void>
|
|
|
|
if (storyContext.parameters?.layout === 'fullscreen') {
|
2023-02-24 20:33:39 +01:00
|
|
|
if (excludeNavigationFromSnapshot) {
|
2023-02-10 12:50:43 +01:00
|
|
|
check = expectStoryToMatchSceneSnapshot
|
|
|
|
} else {
|
|
|
|
check = expectStoryToMatchFullPageSnapshot
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
check = expectStoryToMatchComponentSnapshot
|
|
|
|
}
|
2023-03-02 22:11:03 +01:00
|
|
|
|
2023-02-24 20:33:39 +01:00
|
|
|
// Wait for story to load
|
2023-03-02 22:11:03 +01:00
|
|
|
await page.waitForSelector('.sb-show-preparing-story', { state: 'detached' })
|
2023-03-08 18:06:32 +01:00
|
|
|
await page.evaluate(() => {
|
|
|
|
// Stop all animations for consistent snapshots
|
|
|
|
document.body.classList.add('storybook-test-runner')
|
|
|
|
})
|
2023-02-24 20:33:39 +01:00
|
|
|
if (waitForLoadersToDisappear) {
|
2023-03-02 22:11:03 +01:00
|
|
|
await page.waitForTimeout(300) // Wait for initial UI to load
|
|
|
|
await Promise.all(LOADER_SELECTORS.map((selector) => page.waitForSelector(selector, { state: 'detached' })))
|
|
|
|
if (typeof waitForLoadersToDisappear === 'string') {
|
|
|
|
await page.waitForSelector(waitForLoadersToDisappear)
|
2023-02-24 20:33:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
await page.waitForTimeout(100) // Just a bit of extra delay for things to settle
|
2023-02-10 12:50:43 +01:00
|
|
|
await check(page, context, browser)
|
2023-01-27 15:51:35 +01:00
|
|
|
}
|
|
|
|
|
2023-02-10 12:50:43 +01:00
|
|
|
async function expectStoryToMatchFullPageSnapshot(
|
|
|
|
page: Page,
|
|
|
|
context: TestContext,
|
|
|
|
browser: SupportedBrowserName
|
|
|
|
): Promise<void> {
|
|
|
|
await expectLocatorToMatchStorySnapshot(page, context, browser)
|
2023-01-27 15:51:35 +01:00
|
|
|
}
|
|
|
|
|
2023-02-10 12:50:43 +01:00
|
|
|
async function expectStoryToMatchSceneSnapshot(
|
|
|
|
page: Page,
|
|
|
|
context: TestContext,
|
|
|
|
browser: SupportedBrowserName
|
|
|
|
): Promise<void> {
|
|
|
|
await expectLocatorToMatchStorySnapshot(page.locator('.main-app-content'), context, browser)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function expectStoryToMatchComponentSnapshot(
|
|
|
|
page: Page,
|
|
|
|
context: TestContext,
|
|
|
|
browser: SupportedBrowserName
|
|
|
|
): Promise<void> {
|
2023-01-27 15:51:35 +01:00
|
|
|
await page.evaluate(() => {
|
|
|
|
const rootEl = document.getElementById('root')
|
2023-05-22 09:15:17 +02:00
|
|
|
if (!rootEl) {
|
|
|
|
throw new Error('Could not find root element')
|
2023-01-27 15:51:35 +01:00
|
|
|
}
|
2023-05-22 09:15:17 +02:00
|
|
|
// Make the root element (which is the screenshot reference) hug the component
|
|
|
|
rootEl.style.display = 'inline-block'
|
|
|
|
// If needed, expand the root element so that all popovers are visible in the screenshot
|
|
|
|
document.querySelectorAll('.Popover').forEach((popover) => {
|
|
|
|
const currentRootBoundingClientRect = rootEl.getBoundingClientRect()
|
|
|
|
const popoverBoundingClientRect = popover.getBoundingClientRect()
|
|
|
|
if (popoverBoundingClientRect.right > currentRootBoundingClientRect.right) {
|
|
|
|
rootEl.style.width = `${popoverBoundingClientRect.right}px`
|
|
|
|
}
|
|
|
|
if (popoverBoundingClientRect.bottom > currentRootBoundingClientRect.bottom) {
|
|
|
|
rootEl.style.height = `${popoverBoundingClientRect.bottom}px`
|
|
|
|
}
|
|
|
|
if (popoverBoundingClientRect.top < currentRootBoundingClientRect.top) {
|
|
|
|
rootEl.style.height = `${-popoverBoundingClientRect.top + currentRootBoundingClientRect.bottom}px`
|
|
|
|
}
|
|
|
|
if (popoverBoundingClientRect.left < currentRootBoundingClientRect.left) {
|
|
|
|
rootEl.style.width = `${-popoverBoundingClientRect.left + currentRootBoundingClientRect.right}px`
|
|
|
|
}
|
|
|
|
})
|
|
|
|
// Make the body transparent to take the screenshot without background
|
2023-01-27 15:51:35 +01:00
|
|
|
document.body.style.background = 'transparent'
|
|
|
|
})
|
|
|
|
|
2023-02-10 12:50:43 +01:00
|
|
|
await expectLocatorToMatchStorySnapshot(page.locator('#root'), context, browser, { omitBackground: true })
|
2023-01-27 15:51:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function expectLocatorToMatchStorySnapshot(
|
|
|
|
locator: Locator | Page,
|
|
|
|
context: TestContext,
|
2023-02-10 12:50:43 +01:00
|
|
|
browser: SupportedBrowserName,
|
2023-01-27 15:51:35 +01:00
|
|
|
options?: LocatorScreenshotOptions
|
|
|
|
): Promise<void> {
|
2023-02-03 13:06:21 +01:00
|
|
|
const image = await locator.screenshot({ timeout: 3000, ...options })
|
2023-02-10 12:50:43 +01:00
|
|
|
let customSnapshotIdentifier = context.id
|
|
|
|
if (browser !== 'chromium') {
|
|
|
|
customSnapshotIdentifier += `--${browser}`
|
|
|
|
}
|
2023-01-27 15:51:35 +01:00
|
|
|
expect(image).toMatchImageSnapshot({
|
|
|
|
customSnapshotsDir,
|
2023-02-10 12:50:43 +01:00
|
|
|
customSnapshotIdentifier,
|
|
|
|
// Compare structural similarity instead of raw pixels - reducing false positives
|
|
|
|
// See https://github.com/americanexpress/jest-image-snapshot#recommendations-when-using-ssim-comparison
|
|
|
|
comparisonMethod: 'ssim',
|
2023-03-17 13:26:57 +01:00
|
|
|
failureThreshold: 0.0003,
|
2023-02-10 12:50:43 +01:00
|
|
|
failureThresholdType: 'percent',
|
2023-01-27 15:51:35 +01:00
|
|
|
})
|
|
|
|
}
|