1
0
mirror of https://github.com/element-hq/element-web.git synced 2026-10-24 23:19:26 +03:00
Files
element-web/apps/web/vitest.config.ts
David Langley bc9e9cf28a Strip snapshot css module hashes (#35206)
* Strip CSS module hashes from apps/web snapshots

shared-components and compound-web build their CSS module class names
with postcss-modules' default `_<class>_<hash of the stylesheet>_<line>`,
so any edit to a stylesheet renames every class in it and changes every
apps/web snapshot that renders it. Two unrelated PRs touching the same
stylesheet then always conflict in those snapshots.

Add a snapshot serializer, alongside the React useId one, that reduces
those names to `_<class>` in DOM and string snapshots. The shipped class
names keep their content hash, so two copies of the library loaded at
once (e.g. by a module) stay isolated.

Signed-off-by: David Langley <davidl@element.io>

* Update snapshots for the CSS module hash serializer

Signed-off-by: David Langley <davidl@element.io>

* Only strip CSS module hashes inside class attributes

The serializer ran its regex over the whole snapshot, so text or other
attributes shaped like a CSS module name would have been shortened too.
Restrict it to entries of `class="…"` lists, which is where DOM and HTML
string snapshots put class names, and match each entry whole.

String snapshots are now picked up by a cheap `class="` check, which also
removes the `lastIndex` reset the global regex needed.

Signed-off-by: David Langley <davidl@element.io>

* Explain the CSS module class regexes

Signed-off-by: David Langley <davidl@element.io>

* Strip CSS module hashes by splitting on underscores

Rather than matching the exact `_<class>_<hash>_<line>` shape, drop the
last two `_`-separated parts of any class that starts with `_` and has
all three parts. Simpler to read, and produces the same snapshots.

Signed-off-by: David Langley <davidl@element.io>

---------

Signed-off-by: David Langley <davidl@element.io>
2026-09-30 12:23:21 +00:00

172 lines
7.3 KiB
TypeScript

/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { fileURLToPath } from "node:url";
import { defineProject } from "vitest/config";
import svgr from "vite-plugin-svgr";
import { playwright } from "@vitest/browser-playwright";
import { env, platform } from "node:process";
import rootConfig from "../../vitest.config.ts";
function resolve(specifier: string): string {
return fileURLToPath(import.meta.resolve(specifier));
}
// Tests that need a real browser rather than the happy-dom environment used by
// `vitest.config.ts` — currently those which rasterise to a <canvas> and assert
// on the pixels, which needs a real 2D context and real font metrics.
// Screenshot baselines only mean anything where the rendering is pinned: the
// generic `sans-serif` the badges use resolves to a different typeface on each
// OS, so a macOS baseline would never match CI. We therefore only commit linux
// baselines, and only assert against them when running somewhere that renders
// the same way — CI, or the pinned container that `playwright-screenshots`
// starts (it sets PW_TEST_CONNECT_WS_ENDPOINT). Everywhere else the screenshot
// assertions skip; the rest of the suite still runs.
const wsEndpoint = env["PW_TEST_CONNECT_WS_ENDPOINT"];
const canCompareScreenshots = Boolean(wsEndpoint || env["CI"]);
const browserPlatform = canCompareScreenshots ? "linux" : platform;
export default defineProject({
resolve: {
alias: [
{ find: "test-utils-rtl", replacement: resolve("./src/test/test-utils/vitest-matrix-react") },
{ find: "test-utils", replacement: resolve("./src/test/test-utils") },
// Stub out workers as they do not play well under test
{
find: /.*workers\/(.+)Factory/,
replacement: resolve("./__mocks__/workerFactoryMock.js"),
},
{
find: /.*waveWorker\.min\.js$/,
replacement: resolve("./__mocks__/empty.js"),
},
{
find: /.*decoderWorker\.min\.js$/,
replacement: resolve("./__mocks__/empty.js"),
},
{
find: /.*decoderWorker\.min\.wasm$/,
replacement: resolve("./__mocks__/empty.js"),
},
// Stub this out as we lack AudioWorkletProcessor in the test env
{
find: "./recorderWorkletFactory",
replacement: resolve("./__mocks__/empty.js"),
},
// Stub out legacy modules so we don't need to build them first
{
find: "../modules.js",
replacement: resolve("./__mocks__/empty.js"),
},
],
},
test: {
coverage: {
exclude: ["src/**/*.stories.tsx"],
provider: "v8",
include: ["src/**/*.{ts,tsx}"],
reporter: [["lcov", { projectRoot: "../../" }]],
},
reporters: rootConfig.test?.reporters,
globals: false,
environment: "node",
pool: "threads",
projects: [
{
extends: true,
test: {
name: "unit",
include: ["src/**/*.test.{ts,tsx}"],
setupFiles: ["src/test/setupJsdomPolyfills.ts", "src/test/setupTests.ts"],
environmentOptions: {
happyDOM: {
url: "http://localhost/",
settings: {
navigation: {
// Nothing is served at these URLs under test; an iframe's `src` is just an attribute.
disableChildFrameNavigation: true,
},
},
},
jsdom: {
url: "http://localhost/",
},
},
snapshotSerializers: [
resolve("./src/test/react-use-id-serializer.ts"),
resolve("./src/test/css-module-class-serializer.ts"),
],
},
plugins: [
svgr({
svgrOptions: {
ref: true,
svgProps: { "role": "presentation", "aria-hidden": "true" },
expandProps: "end",
},
}),
],
},
{
extends: true,
test: {
name: "browser",
include: ["src/**/*.test.browser.{ts,tsx}"],
browser: {
enabled: true,
headless: true,
viewport: { width: 960, height: 720 },
provider: playwright({
contextOptions: {
reducedMotion: "reduce",
colorScheme: "light",
deviceScaleFactor: 1,
viewport: { width: 960, height: 720 },
},
launchOptions: {
// Force consistent font rendering, as per packages/shared-components
args: [
"--font-render-hinting=none",
"--disable-font-subpixel-positioning",
"--disable-lcd-text",
],
},
connectOptions: wsEndpoint ? { wsEndpoint, exposeNetwork: "<loopback>" } : undefined,
}),
instances: [{ browser: "chromium" }],
expect: {
toMatchScreenshot: {
comparatorName: "pixelmatch",
comparatorOptions: {
// Enough to absorb sub-pixel antialiasing noise, but far less
// than a clipped or misplaced glyph moves.
allowedMismatchedPixelRatio: 0.02,
},
// Baselines are grouped by platform, and only the linux ones are
// committed — see apps/web/.gitignore.
resolveScreenshotPath: ({
root,
testFileDirectory,
screenshotDirectory,
testFileName,
arg,
ext,
browserName,
}) =>
`${root}/${testFileDirectory}/${screenshotDirectory}/${browserPlatform}/${testFileName}/${arg}-${browserName}${ext}`,
},
},
},
provide: { canCompareScreenshots },
},
},
],
},
});