Skip to content

Commit

Permalink
Add the ability to check two snapshots for each test
Browse files Browse the repository at this point in the history
  • Loading branch information
obulat committed Sep 15, 2024
1 parent cdb7a48 commit 929b892
Show file tree
Hide file tree
Showing 37 changed files with 405 additions and 186 deletions.
38 changes: 38 additions & 0 deletions frontend/.storybook/decorators/with-theme-switcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useDarkMode } from "#imports"

import { ref, watch, onMounted, h } from "vue"

import VThemeSelect from "~/components/VThemeSelect/VThemeSelect.vue"

export const WithThemeSwitcher = (story) => {
return {
components: { story },
setup() {
const element = ref()
const { cssClass } = useDarkMode()

onMounted(() => {
watch(cssClass, async (newClass, oldClass) => {
if (element.value) {
element.value.ownerDocument
.querySelector("#storybook-root")
.classList.add("bg-default")
element.value.ownerDocument.body.classList.add(newClass)
if (oldClass) {
element.value.ownerDocument.body.classList.remove(oldClass)
}
}
})
})
return () =>
h("div", { ref: element }, [
h(story()),
h(
"div",
{ class: "absolute bottom-0", id: "storybook-theme-switcher" },
[h(VThemeSelect)]
),
])
},
}
}
4 changes: 3 additions & 1 deletion frontend/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { VIEWPORTS } from "~/constants/screens"
import { WithUiStore } from "~~/.storybook/decorators/with-ui-store"
import { WithRTL } from "~~/.storybook/decorators/with-rtl"

import { WithThemeSwitcher } from "~~/.storybook/decorators/with-theme-switcher"

import type { Preview } from "@storybook/vue3"

const preview: Preview = {
decorators: [WithRTL, WithUiStore],
decorators: [WithRTL, WithUiStore, WithThemeSwitcher],
globalTypes: {
languageDirection: {
name: "RTL",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { h } from "vue"
import { ImageDetail } from "~/types/media"

import VMediaReuse from "~/components/VMediaInfo/VMediaReuse.vue"
import VLanguageSelect from "~/components/VLanguageSelect/VLanguageSelect.vue"

import type { Meta, StoryObj } from "@storybook/vue3"

Expand Down Expand Up @@ -35,13 +34,9 @@ type Story = StoryObj<typeof meta>

export const Default: Story = {
render: (args) => ({
components: { VMediaReuse, VLanguageSelect },
components: { VMediaReuse },
setup() {
return () =>
h("div", { class: "flex flex-col gap-y-2" }, [
h(VLanguageSelect),
h(VMediaReuse, args),
])
return () => h(VMediaReuse, args)
},
}),
name: "VMediaReuse",
Expand Down
43 changes: 22 additions & 21 deletions frontend/test/playwright/utils/breakpoints.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import { test, expect, Expect } from "@playwright/test"
import { test, Expect, Page, Locator } from "@playwright/test"

import type { LanguageDirection } from "~~/test/playwright/utils/i18n"

import { expectSnapshot as innerExpectSnapshot } from "~~/test/playwright/utils/expect-snapshot"

import { VIEWPORTS } from "~/constants/screens"
import type { Breakpoint } from "~/constants/screens"

type ScreenshotAble = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
screenshot(...args: any[]): Promise<Buffer>
}
type ScreenshotAble = Page | Locator

type ExpectSnapshot = <T extends ScreenshotAble>(
page: Page,
name: string,
s: T,
options?: Parameters<T["screenshot"]>[0],
snapshotOptions?: Parameters<ReturnType<Expect>["toMatchSnapshot"]>[0]
config?: {
dir?: LanguageDirection
options?: Parameters<T["screenshot"]>[0]
snapshotOptions?: Parameters<ReturnType<Expect>["toMatchSnapshot"]>[0]
useColorMode?: boolean
}
) => Promise<Buffer | void>

type BreakpointBlock = (options: {
getConfigValues: (name: string) => {
name: `${typeof name}-${Breakpoint}-light.png`
}
breakpoint: Breakpoint
expectSnapshot: ExpectSnapshot
}) => void
Expand Down Expand Up @@ -87,24 +90,22 @@ const makeBreakpointDescribe =
userAgent: options.uaMocking ? mockUaStrings[breakpoint] : undefined,
})

const getConfigValues = (name: string) => ({
name: `${name}-${breakpoint}-light.png` as const,
})
const getSnapshotName = (name: string, dir?: LanguageDirection) => {
const dirString = dir ? (`-${dir}` as const) : ""
return `${name}${dirString}-${breakpoint}` as const
}

const expectSnapshot = async <T extends ScreenshotAble>(
page: Page,
name: string,
screenshotAble: T,
options?: Parameters<T["screenshot"]>[0],
snapshotOptions?: Parameters<ReturnType<Expect>["toMatchSnapshot"]>[0]
options: Parameters<ExpectSnapshot>[3] = {}
) => {
const { name: snapshotName } = getConfigValues(name)
return expect(await screenshotAble.screenshot(options)).toMatchSnapshot(
snapshotName,
snapshotOptions
)
const snapshotName = getSnapshotName(name, options.dir)
return innerExpectSnapshot(page, snapshotName, screenshotAble, options)
}

_block({ breakpoint, getConfigValues, expectSnapshot })
_block({ breakpoint, expectSnapshot })
})
}

Expand Down
110 changes: 110 additions & 0 deletions frontend/test/playwright/utils/expect-snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { expect } from "@playwright/test"

import { type LanguageDirection, t } from "~~/test/playwright/utils/i18n"

import type { Breakpoint } from "~/constants/screens"

import type {
Expect,
Locator,
LocatorScreenshotOptions,
Page,
PageScreenshotOptions,
} from "@playwright/test"

export type ExpectSnapshotOptions = {
screenshotOptions?: LocatorScreenshotOptions | PageScreenshotOptions
snapshotOptions?: Parameters<ReturnType<Expect>["toMatchSnapshot"]>[0]
dir?: LanguageDirection
useColorMode?: boolean
}
export type ExpectSnapshot = <T extends Locator | Page>(
page: Page,
name: ReturnType<typeof getSnapshotBaseName>,
screenshotAble: T,
options?: ExpectSnapshotOptions
) => Promise<void>

export type ExpectScreenshotAreaSnapshot = (
page: Page,
name: string,
options?: ExpectSnapshotOptions
) => Promise<void>

type EffectiveColorMode = "dark" | "light"
const themeSelectLabel = (dir: LanguageDirection) => t("theme.theme", dir)
const themeOption = (colorMode: EffectiveColorMode, dir: LanguageDirection) =>
t(`theme.choices.${colorMode}`, dir)

export const turnOnDarkMode = async (page: Page, dir: LanguageDirection) => {
// In Storybook, the footer story has two theme switchers (one in the footer, and one
// is from the story decorator), so we need to select a single one.
await page
.getByLabel(themeSelectLabel(dir))
.nth(0)
.selectOption(themeOption("dark", dir))
}

type SnapshotNameOptions = {
dir?: LanguageDirection
breakpoint?: Breakpoint
}

const getSnapshotBaseName = (
name: string,
{ dir, breakpoint }: SnapshotNameOptions = {}
) => {
const dirString = dir ? (`-${dir}` as const) : ""
const breakpointString = breakpoint ? (`-${breakpoint}` as const) : ""
return `${name}${dirString}${breakpointString}` as const
}

const getSnapshotName = (
name: ReturnType<typeof getSnapshotBaseName>,
colorMode: EffectiveColorMode = "light"
) => {
return `${name}-${colorMode}.png` as const
}

/**
* Take a screenshot of the page or a given locator, and compare it to the existing snapshots.
* Take a screenshot in both light and dark mode if `useColorMode` is true.
*/
export const expectSnapshot: ExpectSnapshot = async (
page,
name,
screenshotAble,
{ screenshotOptions, snapshotOptions, useColorMode, dir } = {}
) => {
// Hide the theme switcher before taking the screenshot.
screenshotOptions = {
...(screenshotOptions ?? {}),
style: `#storybook-theme-switcher {
visibility: hidden;
}`,
}

expect
.soft(await screenshotAble.screenshot(screenshotOptions))
.toMatchSnapshot(getSnapshotName(name, "light"), snapshotOptions)

if (!(useColorMode === true)) {
return
}
await turnOnDarkMode(page, dir ?? "ltr")

expect(await screenshotAble.screenshot(screenshotOptions)).toMatchSnapshot(
getSnapshotName(name, "dark"),
snapshotOptions
)
}

/**
* Some component stories have a screenshot area that allows to take a snapshot
* of the area around the component (for focus rings or complex stories with modals
* or popovers).
*/
export const expectScreenshotAreaSnapshot: ExpectScreenshotAreaSnapshot =
async (page, name, options = {}) => {
return expectSnapshot(page, name, page.locator(".screenshot-area"), options)
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ test.describe("content report form", () => {

await button.click()

await expectSnapshot("content-report", page, undefined, {
maxDiffPixelRatio: 0.1,
await expectSnapshot(page, "content-report", page, {
snapshotOptions: { maxDiffPixelRatio: 0.1 },
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@ for (const dir of languageDirections) {
await page.mouse.move(0, 0)

await expectSnapshot(
`external-${mediaType}-sources-popover-${dir}`,
page,
`external-${mediaType}-sources-popover`,
page.getByRole("dialog"),
{},
{ maxDiffPixelRatio: 0.01, maxDiffPixels: undefined }
{
dir,
snapshotOptions: {
maxDiffPixelRatio: 0.01,
maxDiffPixels: undefined,
},
}
)
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,32 @@ for (const dir of languageDirections) {
await filters.open(page, dir)
})
test(`filters modal none selected - ${dir}`, async ({ page }) => {
const snapshotName = `${getFiltersName(breakpoint)}-${dir}`

await expectSnapshot(
snapshotName,
page,
getFiltersName(breakpoint),
isDesktop ? page.locator(".sidebar") : page,
{},
{ maxDiffPixels: 2, maxDiffPixelRatio: undefined }
{
dir,
snapshotOptions: { maxDiffPixels: 2, maxDiffPixelRatio: undefined },
}
)
})

test(`filters modal 1 filter selected - ${dir}`, async ({ page }) => {
const firstFilter = page.getByRole("checkbox").first()
await firstFilter.check()

const snapshotName = `${getFiltersName(breakpoint)}-checked-${dir}`
const snapshotName = `${getFiltersName(breakpoint)}-checked`

await firstFilter.hover()
await expectSnapshot(
page,
snapshotName,
isDesktop ? page.locator(".sidebar") : page,
{},
{ maxDiffPixels: 2, maxDiffPixelRatio: undefined }
{
dir,
snapshotOptions: { maxDiffPixels: 2, maxDiffPixelRatio: undefined },
}
)
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ for (const dir of languageDirections) {
// To make the tests consistent, set the played area to the same position
await page.mouse.click(170, 650)

await expectSnapshot(`global-audio-player-on-search-${dir}`, page)
await expectSnapshot(page, "global-audio-player-on-search", page, { dir })
})
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ for (const dir of languageDirections) {
test("filters open", async ({ page }) => {
await page.mouse.move(0, 150)
await expectSnapshot(
`filters-open-${dir}`,
page.locator(headerSelector)
page,
"filters-open",
page.locator(headerSelector),
{ dir }
)
})

Expand All @@ -45,22 +47,28 @@ for (const dir of languageDirections) {
test("resting", async ({ page }) => {
// Make sure the header is not hovered on
await page.mouse.move(0, 150)
await expectSnapshot(`resting-${dir}`, page.locator(headerSelector))
await expectSnapshot(page, "resting", page.locator(headerSelector), {
dir,
})
})

test("scrolled", async ({ page }) => {
await scrollToBottom(page)
await page.mouse.move(0, 150)
await sleep(200)
await expectSnapshot(`scrolled-${dir}`, page.locator(headerSelector))
await expectSnapshot(page, "scrolled", page.locator(headerSelector), {
dir,
})
})

test("searchbar hovered", async ({ page }) => {
await page.hover("input")
await hideInputCursors(page)
await expectSnapshot(
`searchbar-hovered-${dir}`,
page.locator(headerSelector)
page,
"searchbar-hovered",
page.locator(headerSelector),
{ dir }
)
})

Expand All @@ -74,7 +82,7 @@ for (const dir of languageDirections) {
const locator = isMobileBreakpoint(breakpoint)
? page
: page.locator(headerSelector)
await expectSnapshot(`searchbar-active-${dir}`, locator)
await expectSnapshot(page, "searchbar-active", locator, { dir })
})
})
})
Expand Down
Loading

0 comments on commit 929b892

Please sign in to comment.