-
Notifications
You must be signed in to change notification settings - Fork 194
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enable running the snapshot tests for both color modes (#4926)
* Add the ability to check two snapshots for each test * Add the correct RTL snapshots for collection headers * Add the toolbar button for theme switching * Fix the image cell snapshot wrapper * Refactor WithTheme decorator * Add whitespace * Fix footer test
- Loading branch information
Showing
62 changed files
with
480 additions
and
199 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { watch, onMounted, reactive, h } from "vue" | ||
import { useEffect, useGlobals } from "@storybook/preview-api" | ||
|
||
import { EffectiveColorMode } from "~/types/ui" | ||
|
||
import { useDarkMode } from "~/composables/use-dark-mode" | ||
import { useUiStore } from "~/stores/ui" | ||
|
||
import VThemeSelect from "~/components/VThemeSelect/VThemeSelect.vue" | ||
|
||
type ThemeCssClass = `${EffectiveColorMode}-mode` | ||
const cssClassToTheme = ( | ||
cssClass: ThemeCssClass | undefined | ||
): EffectiveColorMode | undefined => cssClass?.split("-")[0] | ||
const isEffectiveColorMode = ( | ||
value: string | undefined | ||
): value is EffectiveColorMode => ["light", "dark"].includes(value) | ||
|
||
const setElementTheme = (el: HTMLElement, cssClass: ThemeCssClass) => { | ||
if (cssClass === "dark-mode") { | ||
el.classList.add("dark-mode") | ||
el.classList.remove("light-mode") | ||
} else { | ||
el.classList.add("light-mode") | ||
el.classList.remove("dark-mode") | ||
} | ||
} | ||
const themeState = reactive<{ value: EffectiveColorMode }>({ value: "light" }) | ||
|
||
/** | ||
* Decorator to add the Storybook theme switcher to the addon toolbar, and the Openverse | ||
* theme switcher to the bottom of the screen. | ||
* We cannot use the toolbar during the tests that open an iframe without the toolbars, | ||
* so we need to add the theme switcher to the bottom of the screen. | ||
* The state of both is kept in sync. | ||
*/ | ||
export const WithTheme = (story) => { | ||
const [globals, updateGlobals] = useGlobals() | ||
themeState.value = globals.theme | ||
|
||
useEffect(() => { | ||
themeState.value = globals.theme | ||
}, [globals.theme]) | ||
|
||
return { | ||
components: { story }, | ||
setup() { | ||
const { cssClass } = useDarkMode() | ||
const uiStore = useUiStore() | ||
|
||
watch( | ||
themeState, | ||
(newTheme) => { | ||
if (isEffectiveColorMode(newTheme.value)) { | ||
uiStore.setColorMode(newTheme.value) | ||
} | ||
}, | ||
{ immediate: true } | ||
) | ||
|
||
watch( | ||
cssClass, | ||
(newCssClass) => { | ||
setElementTheme(document.body, newCssClass) | ||
const theme = cssClassToTheme(newCssClass) | ||
if (theme) { | ||
updateGlobals({ theme }) | ||
} | ||
}, | ||
{ immediate: true } | ||
) | ||
|
||
onMounted(() => { | ||
document.body.classList.add("bg-default") | ||
}) | ||
|
||
// Set the height to the full height of the Storybook iframe minus the padding | ||
// to position the theme switcher at the bottom of the screen. | ||
return () => | ||
h("div", { class: "relative", style: "height: calc(100dvh - 32px);" }, [ | ||
h(story()), | ||
h( | ||
"div", | ||
{ class: "absolute bottom-0", id: "storybook-theme-switcher" }, | ||
[h(VThemeSelect)] | ||
), | ||
]) | ||
}, | ||
} | ||
} |
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
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) | ||
} |
This file contains 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
This file contains 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
Oops, something went wrong.