diff --git a/.changeset/config.json b/.changeset/config.json index ecd96b68..d3a70fcb 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -10,4 +10,4 @@ "snapshot": { "useCalculatedVersion": true } -} \ No newline at end of file +} diff --git a/.changeset/esm-only-node-22.md b/.changeset/esm-only-node-22.md index 5193c037..5493234c 100644 --- a/.changeset/esm-only-node-22.md +++ b/.changeset/esm-only-node-22.md @@ -1,6 +1,6 @@ --- -'@chromatic-com/cypress': minor -'@chromatic-com/playwright': minor +"@chromatic-com/cypress": minor +"@chromatic-com/playwright": minor --- Feat: Convert all packages to ESM-only, require Node.js 22 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8786bfe5..038e8ccb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,9 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' -labels: '' -assignees: '' +title: "" +labels: "" +assignees: "" --- **Describe the bug** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2f28cead..2bc5d5f7 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,9 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' +title: "" +labels: "" +assignees: "" --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a2aea75..0b7c9072 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ name: Release on: workflow_run: - workflows: ['Test and Release Main'] + workflows: ["Test and Release Main"] types: [completed] permissions: @@ -45,8 +45,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' - registry-url: 'https://registry.npmjs.org' + node-version-file: ".nvmrc" + registry-url: "https://registry.npmjs.org" cache: pnpm - name: Install Dependencies diff --git a/.github/workflows/test-base.yml b/.github/workflows/test-base.yml index f7e8c4a5..d2640374 100644 --- a/.github/workflows/test-base.yml +++ b/.github/workflows/test-base.yml @@ -21,7 +21,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" cache: pnpm - name: Install Dependencies @@ -32,12 +32,19 @@ jobs: working-directory: ./embedded - name: Build - run: | - pnpm build + run: pnpm build - name: Publish canary run: pnpm dlx pkg-pr-new publish --commentWithDev --commentWithSha --packageManager pnpm './packages/cypress' './packages/playwright' './packages/vitest' + - name: Run format + run: pnpm fmt --check + + - name: Run lint + run: | + pnpm lint + pnpm publint + - uses: ./.github/actions/setup-playwright # Needed for additional browsers that Vitest unit tests use. @@ -45,14 +52,8 @@ jobs: - name: Playwright deps run: pnpm exec playwright install-deps - - name: Run lint - run: | - pnpm lint - pnpm publint - - name: Run tests - run: | - pnpm test:unit + run: pnpm test:unit test-vitest-4: runs-on: ubuntu-latest @@ -67,7 +68,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" cache: pnpm - name: Override Vitest to v4 diff --git a/.github/workflows/test-cypress.yml b/.github/workflows/test-cypress.yml index 44d5193a..fde4f15b 100644 --- a/.github/workflows/test-cypress.yml +++ b/.github/workflows/test-cypress.yml @@ -21,7 +21,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" cache: pnpm - name: Install Dependencies diff --git a/.github/workflows/test-playwright.yml b/.github/workflows/test-playwright.yml index b5a91bf4..c5b96962 100644 --- a/.github/workflows/test-playwright.yml +++ b/.github/workflows/test-playwright.yml @@ -24,7 +24,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" cache: pnpm - name: Install Dependencies diff --git a/.github/workflows/test-vitest.yml b/.github/workflows/test-vitest.yml index 7281895e..1bb2d866 100644 --- a/.github/workflows/test-vitest.yml +++ b/.github/workflows/test-vitest.yml @@ -24,7 +24,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" cache: pnpm - name: Install Dependencies diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 1521c8b7..00000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index d033feea..00000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 100, - "tabWidth": 2, - "bracketSpacing": true, - "trailingComma": "es5", - "singleQuote": true, - "arrowParens": "always" -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa6215..3ffabfc9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,9 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "editor.defaultFormatter": "oxc.oxc-vscode", + "[typescript]": { + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + }, + "editor.defaultFormatter": "oxc.oxc-vscode" + } } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f6e448db..fdc65770 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -49,14 +49,14 @@ If you wish to run the site-under-tests's server separately (e.g. to debug a spe ### Linting & formatting -We use `eslint` and `prettier` to ensure code consistency. +We use `oxlint` and `oxfmt` to ensure code consistency. To have all formatting and linting auto-fixed (so much as possible) run this at the project root: ``` pnpm lint --fix -pnpm prettier . --write +pnpm fmt ``` ### PR Workflow with Changesets diff --git a/embedded/package.json b/embedded/package.json index 947e3e4d..43376024 100644 --- a/embedded/package.json +++ b/embedded/package.json @@ -4,10 +4,10 @@ "private": true, "license": "MIT", "author": "Chromatic ", - "packageManager": "npm@11.15.0", "dependencies": { "@storybook/builder-webpack5": "10.2.13", "@storybook/server": "10.2.13", "@storybook/server-webpack5": "10.2.13" - } + }, + "packageManager": "npm@11.15.0" } diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index d5d3a745..00000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import js from '@eslint/js'; -import tseslint from 'typescript-eslint'; -import globals from 'globals'; -import eslintConfigPrettier from 'eslint-config-prettier'; - -export default tseslint.config( - { - ignores: [ - '**/node_modules/**', - '**/test-results/**', - '**/dist/**', - '**/.cache/**', - '**/*.d.ts', - '**/storybook-static/**', - '**/tsdown.config.ts', - '**/playwright.config.ts', - '**/tests/**', - '**/__playwright-tests__/**', - 'test-server/fixtures/**', - ], - }, - js.configs.recommended, - ...tseslint.configs.recommended, - { - files: ['**/*.js'], - languageOptions: { - globals: { ...globals.node }, - sourceType: 'module', - parserOptions: { ecmaVersion: 'latest' }, - }, - }, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - globals: { ...globals.node }, - }, - rules: { - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - }, - }, - { - files: ['**/*.test.ts', '**/*.spec.ts'], - rules: { - 'no-restricted-syntax': 'off', - 'no-empty-pattern': 'off', - }, - }, - eslintConfigPrettier -); diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 00000000..f8122369 --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "oxfmt"; + +export default defineConfig({ + sortImports: true, + ignorePatterns: ["**/fixtures/corrupt-stats/*.json"], +}); diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 00000000..53d69b54 --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "oxlint"; + +export default defineConfig({ + options: { + typeAware: true, + }, + + rules: { + "unicorn/prefer-node-protocol": "error", + }, + + overrides: [ + // Tests + { + files: ["**/**.{test,test-d}.ts", "**/{test,tests,test-server}/**"], + rules: { + "typescript/unbound-method": "off", + "typescript/no-floating-promises": "off", + "typescript/require-array-sort-compare": "off", + "no-empty-pattern": "off", + "no-unused-expressions": "off", + "unicorn/no-empty-file": "off", + }, + }, + ], +}); diff --git a/package.json b/package.json index 3e3a0c8b..8f442f81 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,13 @@ "version": "0.0.56", "private": true, "description": "Archive end-to-end tests to be replayed in Storybook and Chromatic", + "license": "MIT", + "author": "Chromatic ", "repository": { "type": "git", "url": "https://github.com/chromaui/chromatic-e2e.git" }, - "license": "MIT", - "author": "Chromatic ", "type": "module", - "engines": { - "node": ">=22" - }, "scripts": { "archive-storybook:cypress": "CHROMATIC_ARCHIVE_LOCATION=tests/cypress/test-downloads pnpm -C packages/cypress exec dist/bin/archive-storybook.mjs", "archive-storybook:playwright": "CHROMATIC_ARCHIVE_LOCATION=test-results pnpm -C packages/playwright exec dist/bin/archive-storybook.mjs", @@ -28,22 +25,18 @@ "build-archive-storybook:vitest": "CHROMATIC_ARCHIVE_LOCATION=test/.vitest/chromatic pnpm -C packages/vitest exec dist/bin/build-archive-storybook.mjs", "changeset": "changeset", "dev:server": "node test-server/server", - "lint": "eslint .", + "lint": "oxlint", + "fmt": "oxfmt", "publint": "pnpm -r --parallel --filter !@chromatic-com/shared-e2e exec publint", "prepare": "husky install", - "prettier": "prettier", "release": "pnpm build && pnpm changeset publish", "test:unit": "vitest run --configLoader=runner", "test:cypress": "pnpm --filter @chromatic-com/cypress test:cypress", "test:playwright": "pnpm --filter @chromatic-com/playwright test:playwright", "test:vitest": "pnpm --filter @chromatic-com/vitest test:vitest" }, - "lint-staged": { - "*.{ts,js,css,md}": "prettier --write" - }, "devDependencies": { "@changesets/cli": "^2.31.1", - "@eslint/js": "^9.15.0", "@testing-library/dom": "^8.1.0", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", @@ -52,19 +45,24 @@ "@types/node": "^22.0.0", "@types/node-fetch": "^2.6.6", "@vitest/coverage-v8": "^5.0.0", - "eslint": "^9.15.0", - "eslint-config-prettier": "^9.1.0", "express": "^4.18.2", "express-basic-auth": "^1.2.1", "globals": "^15.12.0", "husky": ">=6", "lint-staged": ">=10", + "oxfmt": "^0.65.0", + "oxlint": "^1.80.0", + "oxlint-tsgolint": "^7.0.2001", "playwright": "^1.46.1", - "prettier": "^3.5.3", "publint": "^0.3.24", "typescript": "^5.9.3", - "typescript-eslint": "^8.15.0", "vitest": "^5.0.0" }, + "lint-staged": { + "*.{ts,js,css,md}": "oxfmt" + }, + "engines": { + "node": ">=22" + }, "packageManager": "pnpm@11.5.3" } diff --git a/packages/cypress/embedded.test.ts b/packages/cypress/embedded.test.ts index e30e2f6b..8092d308 100644 --- a/packages/cypress/embedded.test.ts +++ b/packages/cypress/embedded.test.ts @@ -1,19 +1,19 @@ -import { expect, it } from 'vitest'; import { collectEmbeddedFiles, generateEmbeddedChecksum, -} from '@chromatic-com/shared-e2e/test-utils'; +} from "@chromatic-com/shared-e2e/test-utils"; +import { expect, it } from "vitest"; -it('/embedded/node_modules has not changed', () => { +it("/embedded/node_modules has not changed", () => { const { hash, fileCount } = generateEmbeddedChecksum(import.meta.dirname); expect(fileCount).toMatchInlineSnapshot(`8198`); expect(hash).toMatchInlineSnapshot( - `"fb7a3c92fbe7930617d966322c4b20b3918a3e197422c2850b6f8838a9d8ad8c"` + `"fb7a3c92fbe7930617d966322c4b20b3918a3e197422c2850b6f8838a9d8ad8c"`, ); }); -it('/embedded/node_modules tree snapshot', () => { +it("/embedded/node_modules tree snapshot", () => { expect(collectEmbeddedFiles(import.meta.dirname)).toMatchInlineSnapshot(` [ "@babel/code-frame@7.29.7", diff --git a/packages/cypress/package.json b/packages/cypress/package.json index fb6e7ffa..af08907c 100644 --- a/packages/cypress/package.json +++ b/packages/cypress/package.json @@ -2,17 +2,25 @@ "name": "@chromatic-com/cypress", "version": "0.13.2", "description": "Chromatic Visual Regression Testing for Cypress", + "license": "MIT", + "author": "Chromatic ", "repository": { "type": "git", "url": "git+https://github.com/chromaui/chromatic-e2e.git", "directory": "packages/cypress" }, - "license": "MIT", - "author": "Chromatic ", - "type": "module", - "engines": { - "node": ">=22" + "bin": { + "archive-storybook": "dist/bin/archive-storybook.mjs", + "build-archive-storybook": "dist/bin/build-archive-storybook.mjs" }, + "files": [ + "dist", + "embedded", + "README.md" + ], + "type": "module", + "main": "dist/index.mjs", + "types": "dist/index.d.mts", "exports": { ".": { "types": "./dist/index.d.mts", @@ -26,17 +34,9 @@ "./bin/build-archive-storybook": "./dist/bin/build-archive-storybook.mjs", "./package.json": "./package.json" }, - "main": "dist/index.mjs", - "types": "dist/index.d.mts", - "bin": { - "archive-storybook": "dist/bin/archive-storybook.mjs", - "build-archive-storybook": "dist/bin/build-archive-storybook.mjs" + "publishConfig": { + "access": "public" }, - "files": [ - "dist", - "embedded", - "README.md" - ], "scripts": { "build": "tsdown", "test:cypress": "start-server-and-test 'pnpm --filter @chromaui/chromatic-e2e dev:server' 3000 'pnpm test:do-cypress'", @@ -61,7 +61,7 @@ "peerDependencies": { "cypress": ">=15.10.0" }, - "publishConfig": { - "access": "public" + "engines": { + "node": ">=22" } } diff --git a/packages/cypress/src/bin/archive-storybook.ts b/packages/cypress/src/bin/archive-storybook.ts index 5fdf5cfd..fdda0b8c 100644 --- a/packages/cypress/src/bin/archive-storybook.ts +++ b/packages/cypress/src/bin/archive-storybook.ts @@ -1,12 +1,14 @@ #!/usr/bin/env node -import { archiveStorybook } from '@chromatic-com/shared-e2e/archive-storybook'; -import path from 'path'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; +import path from "node:path"; + +import { archiveStorybook } from "@chromatic-com/shared-e2e/archive-storybook"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; // Discard first two entries (exec path and file path) const args = process.argv.slice(2); -const configDir = path.resolve(import.meta.dirname, '../storybook-config'); +const configDir = path.resolve(import.meta.dirname, "../storybook-config"); try { await archiveStorybook(args, configDir, DEFAULT_OUTPUT_DIR); diff --git a/packages/cypress/src/bin/build-archive-storybook.ts b/packages/cypress/src/bin/build-archive-storybook.ts index 38e4dc88..e3a50a4a 100644 --- a/packages/cypress/src/bin/build-archive-storybook.ts +++ b/packages/cypress/src/bin/build-archive-storybook.ts @@ -1,12 +1,14 @@ #!/usr/bin/env node -import { buildArchiveStorybook } from '@chromatic-com/shared-e2e/archive-storybook'; -import path from 'path'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; +import path from "node:path"; + +import { buildArchiveStorybook } from "@chromatic-com/shared-e2e/archive-storybook"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; // Discard first two entries (exec path and file path) const args = process.argv.slice(2); -const configDir = path.resolve(import.meta.dirname, '../storybook-config'); +const configDir = path.resolve(import.meta.dirname, "../storybook-config"); try { await buildArchiveStorybook(args, configDir, DEFAULT_OUTPUT_DIR); diff --git a/packages/cypress/src/commands.ts b/packages/cypress/src/commands.ts index 6d089889..7e4c5efc 100644 --- a/packages/cypress/src/commands.ts +++ b/packages/cypress/src/commands.ts @@ -1,5 +1,5 @@ -import { takeSnapshot as takeChromaticSnapshot } from './takeSnapshot'; -import { CypressSnapshot } from './types'; +import { takeSnapshot as takeChromaticSnapshot } from "./takeSnapshot"; +import { CypressSnapshot } from "./types"; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -16,15 +16,15 @@ declare global { } } -Cypress.Commands.add('takeSnapshot', (name?: string) => { +Cypress.Commands.add("takeSnapshot", (name?: string) => { // don't take snapshots when running `cypress open` - if (!Cypress.config('isTextTerminal')) { + if (!Cypress.config("isTextTerminal")) { return; } cy.window().then((win) => { const viewport = { width: win.innerWidth, height: win.innerHeight }; - const colorScheme = win.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const colorScheme = win.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; cy.document().then((doc) => { // here, handle the source map @@ -32,12 +32,12 @@ Cypress.Commands.add('takeSnapshot', (name?: string) => { cy.wrap(takeChromaticSnapshot(doc, viewport, colorScheme, true)).then( (manualSnapshot: CypressSnapshot) => { // reassign manualSnapshots so it includes this new snapshot - cy.get('@manualSnapshots') + cy.get("@manualSnapshots") .then((snapshots: CypressSnapshot[]) => { return [...snapshots, { ...manualSnapshot, name }]; }) - .as('manualSnapshots'); - } + .as("manualSnapshots"); + }, ); }); }); diff --git a/packages/cypress/src/constants.ts b/packages/cypress/src/constants.ts index a98daad2..53c72f5b 100644 --- a/packages/cypress/src/constants.ts +++ b/packages/cypress/src/constants.ts @@ -1 +1 @@ -export const DEFAULT_OUTPUT_DIR = 'cypress/downloads'; +export const DEFAULT_OUTPUT_DIR = "cypress/downloads"; diff --git a/packages/cypress/src/index.ts b/packages/cypress/src/index.ts index c8200171..48dc6136 100644 --- a/packages/cypress/src/index.ts +++ b/packages/cypress/src/index.ts @@ -1,12 +1,13 @@ -import CDP, { Version } from 'chrome-remote-interface'; import { ResourceArchiver, writeTestResult, ChromaticStorybookParameters, ResourceArchive, Viewport, -} from '@chromatic-com/shared-e2e'; -import { CypressSnapshot } from './types'; +} from "@chromatic-com/shared-e2e"; +import CDP, { Version } from "chrome-remote-interface"; + +import { CypressSnapshot } from "./types"; interface WriteParams { testTitlePath: string[]; @@ -34,7 +35,7 @@ const writeArchives = async ({ domSnapshots.map(({ name, snapshot, viewport, colorScheme, pseudoClassIds }, index) => [ name ?? `Snapshot #${index + 1}`, { snapshot: Buffer.from(JSON.stringify(snapshot)), viewport, colorScheme, pseudoClassIds }, - ]) + ]), ); await writeTestResult( @@ -46,7 +47,7 @@ const writeArchives = async ({ allSnapshots, resourceArchive, chromaticStorybookParams, - 'cypress' + "cypress", ); }; @@ -56,7 +57,7 @@ const writeArchives = async ({ // Cypress doesn't give us a way to share variables between the "before test" and "after test" lifecycle events on the server. let resourceArchiver: ResourceArchiver = null; -let host = ''; +let host = ""; let port = 0; const setupNetworkListener = async ({ @@ -79,7 +80,7 @@ const setupNetworkListener = async ({ await resourceArchiver.watch(); } } catch (err) { - console.log('err', err); + console.log("err", err); } return null; @@ -98,7 +99,7 @@ const saveArchives = (archiveInfo: WriteParams) => { }; interface TaskParams { - action: 'setup-network-listener' | 'save-archives'; + action: "setup-network-listener" | "save-archives"; payload?: any; } @@ -107,9 +108,9 @@ interface TaskParams { // (they can just import and register prepareArchives) export const prepareArchives = async ({ action, payload }: TaskParams) => { switch (action) { - case 'setup-network-listener': + case "setup-network-listener": return setupNetworkListener(payload); - case 'save-archives': + case "save-archives": return saveArchives(payload); default: return null; @@ -122,31 +123,31 @@ export const onBeforeBrowserLaunch = ( // (this way users wouldn't have to change their cypress.config file as it's already passed to us) browser: Cypress.Browser, launchOptions: Cypress.BeforeBrowserLaunchOptions, - config: Cypress.PluginConfigOptions + config: Cypress.PluginConfigOptions, ) => { // don't take snapshots when running `cypress open` if (!config.isTextTerminal) { return launchOptions; } - const hostArg = launchOptions.args.find((arg) => arg.startsWith('--remote-debugging-address=')); - host = hostArg ? hostArg.split('=')[1] : '127.0.0.1'; + const hostArg = launchOptions.args.find((arg) => arg.startsWith("--remote-debugging-address=")); + host = hostArg ? hostArg.split("=")[1] : "127.0.0.1"; - const portArg = launchOptions.args.find((arg) => arg.startsWith('--remote-debugging-port=')); - let portString = ''; + const portArg = launchOptions.args.find((arg) => arg.startsWith("--remote-debugging-port=")); + let portString = ""; if (portArg) { - [, portString] = portArg.split('='); + [, portString] = portArg.split("="); } else if (process.env.ELECTRON_EXTRA_LAUNCH_ARGS) { // Electron doesn't pass along the address and port in the launch options, so we need to read the port from the // environment variable that we'll require the user to use (this assumes the host will be 127.0.0.1). - const entry = process.env.ELECTRON_EXTRA_LAUNCH_ARGS.split(' ').find((item) => - item.startsWith('--remote-debugging-port') + const entry = process.env.ELECTRON_EXTRA_LAUNCH_ARGS.split(" ").find((item) => + item.startsWith("--remote-debugging-port"), ); - [, portString] = entry.split('='); + [, portString] = entry.split("="); } else { throw new Error( - 'Please provide a port number \nExample: ELECTRON_EXTRA_LAUNCH_ARGS=--remote-debugging-port= yarn cypress run' + "Please provide a port number \nExample: ELECTRON_EXTRA_LAUNCH_ARGS=--remote-debugging-port= yarn cypress run", ); } @@ -157,13 +158,13 @@ export const onBeforeBrowserLaunch = ( export const installPlugin = (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => { // these events are run on the server (in Node) - on('task', { + on("task", { prepareArchives, }); on( - 'before:browser:launch', + "before:browser:launch", (browser: Cypress.Browser, launchOptions: Cypress.BeforeBrowserLaunchOptions) => { onBeforeBrowserLaunch(browser, launchOptions, config); - } + }, ); }; diff --git a/packages/cypress/src/network-idle-watcher.test.ts b/packages/cypress/src/network-idle-watcher.test.ts index aac5db23..41c61c5a 100644 --- a/packages/cypress/src/network-idle-watcher.test.ts +++ b/packages/cypress/src/network-idle-watcher.test.ts @@ -1,14 +1,15 @@ -import { expect, it, vi } from 'vitest'; -import { NetworkIdleWatcher } from './network-idle-watcher'; +import { expect, it, vi } from "vitest"; + +import { NetworkIdleWatcher } from "./network-idle-watcher"; vi.useFakeTimers(); -it('Resolves when there is no network activity', async () => { +it("Resolves when there is no network activity", async () => { const watcher = new NetworkIdleWatcher(); await expect(watcher.idle()).resolves.toBeDefined(); }); -it('Resolves when there is a single request and response', async () => { +it("Resolves when there is a single request and response", async () => { const watcher = new NetworkIdleWatcher(); watcher.onRequest(); @@ -17,7 +18,7 @@ it('Resolves when there is a single request and response', async () => { await expect(watcher.idle()).resolves.toBeDefined(); }); -it('Resolves when there are an equal amount of requests and responses', async () => { +it("Resolves when there are an equal amount of requests and responses", async () => { const watcher = new NetworkIdleWatcher(); // in total 4 requests, and 4 responses watcher.onRequest(); @@ -35,7 +36,7 @@ it('Resolves when there are an equal amount of requests and responses', async () await expect(watcher.idle()).resolves.toBeDefined(); }); -it('Rejects if response never sent for request', async () => { +it("Rejects if response never sent for request", async () => { const watcher = new NetworkIdleWatcher(); // fire off request watcher.onRequest(); diff --git a/packages/cypress/src/network-idle-watcher.ts b/packages/cypress/src/network-idle-watcher.ts index 1c2b9387..3d48abf2 100644 --- a/packages/cypress/src/network-idle-watcher.ts +++ b/packages/cypress/src/network-idle-watcher.ts @@ -16,7 +16,7 @@ export class NetworkIdleWatcher { resolve(true); } else { this.idleTimer = setTimeout(() => { - reject(new Error('some responses have not returned')); + reject(new Error("some responses have not returned")); }, TOTAL_TIMEOUT_DURATION); // assign a function that'll be called as soon as responses are all back diff --git a/packages/cypress/src/storybook-config/main.ts b/packages/cypress/src/storybook-config/main.ts index a8d13d02..750a4b3e 100644 --- a/packages/cypress/src/storybook-config/main.ts +++ b/packages/cypress/src/storybook-config/main.ts @@ -1,30 +1,32 @@ -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; -import type { StorybookConfig } from '@storybook/server-webpack5'; -import { archivesDir } from '@chromatic-com/shared-e2e/utils/filePaths'; -import { storybookParentNodeModulesDir } from '@chromatic-com/shared-e2e/utils/storybookPaths'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { archivesDir } from "@chromatic-com/shared-e2e/utils/filePaths"; +import { storybookParentNodeModulesDir } from "@chromatic-com/shared-e2e/utils/storybookPaths"; +import type { StorybookConfig } from "@storybook/server-webpack5"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; // Resolve embedded Storybook 10.x stack from package root (dist/storybook-config -> .. -> ..) -const packageRoot = path.resolve(import.meta.dirname, '..', '..'); -const embeddedDir = path.join(packageRoot, 'embedded', 'node_modules'); +const packageRoot = path.resolve(import.meta.dirname, "..", ".."); +const embeddedDir = path.join(packageRoot, "embedded", "node_modules"); export default { - stories: [path.resolve(archivesDir(DEFAULT_OUTPUT_DIR), '*.stories.json')], - managerEntries: [path.resolve(import.meta.dirname, 'manager.mjs')], - previewAnnotations: [path.resolve(import.meta.dirname, 'preview.mjs')], + stories: [path.resolve(archivesDir(DEFAULT_OUTPUT_DIR), "*.stories.json")], + managerEntries: [path.resolve(import.meta.dirname, "manager.mjs")], + previewAnnotations: [path.resolve(import.meta.dirname, "preview.mjs")], framework: { - name: path.join(embeddedDir, '@storybook', 'server-webpack5'), + name: path.join(embeddedDir, "@storybook", "server-webpack5"), options: {}, }, core: { // ESM does not support directory imports; point to the package entry file. builder: pathToFileURL( - path.join(embeddedDir, '@storybook', 'builder-webpack5', 'dist', 'index.js') + path.join(embeddedDir, "@storybook", "builder-webpack5", "dist", "index.js"), ).href, - renderer: pathToFileURL(path.join(embeddedDir, '@storybook', 'server', 'preset.js')).href, + renderer: pathToFileURL(path.join(embeddedDir, "@storybook", "server", "preset.js")).href, }, - staticDirs: [path.resolve(archivesDir(DEFAULT_OUTPUT_DIR), 'archive')], + staticDirs: [path.resolve(archivesDir(DEFAULT_OUTPUT_DIR), "archive")], features: { sidebarOnboardingChecklist: false, }, @@ -36,7 +38,7 @@ export default { if (storybookNodeModules) { config.resolve ??= {}; config.resolve.modules = [ - ...(config.resolve.modules ?? ['node_modules']), + ...(config.resolve.modules ?? ["node_modules"]), storybookNodeModules, ]; } diff --git a/packages/cypress/src/support.ts b/packages/cypress/src/support.ts index f6a3de29..fdb47dd8 100644 --- a/packages/cypress/src/support.ts +++ b/packages/cypress/src/support.ts @@ -1,67 +1,67 @@ -import './commands'; -import { takeSnapshot } from './takeSnapshot'; -import { CypressSnapshot } from './types'; +import "./commands"; +import { takeSnapshot } from "./takeSnapshot"; +import { CypressSnapshot } from "./types"; -const buildChromaticParams = (expose: Cypress.Cypress['expose']) => ({ - ...(expose('diffThreshold') && { - diffThreshold: expose('diffThreshold'), +const buildChromaticParams = (expose: Cypress.Cypress["expose"]) => ({ + ...(expose("diffThreshold") && { + diffThreshold: expose("diffThreshold"), }), - ...(expose('delay') && { delay: expose('delay') }), - ...(expose('diffIncludeAntiAliasing') && { - diffIncludeAntiAliasing: expose('diffIncludeAntiAliasing'), + ...(expose("delay") && { delay: expose("delay") }), + ...(expose("diffIncludeAntiAliasing") && { + diffIncludeAntiAliasing: expose("diffIncludeAntiAliasing"), }), - ...(expose('diffThreshold') && { - diffThreshold: expose('diffThreshold'), + ...(expose("diffThreshold") && { + diffThreshold: expose("diffThreshold"), }), - ...(expose('forcedColors') && { forcedColors: expose('forcedColors') }), - ...(expose('pauseAnimationAtEnd') && { - pauseAnimationAtEnd: expose('pauseAnimationAtEnd'), + ...(expose("forcedColors") && { forcedColors: expose("forcedColors") }), + ...(expose("pauseAnimationAtEnd") && { + pauseAnimationAtEnd: expose("pauseAnimationAtEnd"), }), - ...(expose('prefersReducedMotion') && { - prefersReducedMotion: expose('prefersReducedMotion'), + ...(expose("prefersReducedMotion") && { + prefersReducedMotion: expose("prefersReducedMotion"), }), - ...(expose('cropToViewport') && { - cropToViewport: expose('cropToViewport'), + ...(expose("cropToViewport") && { + cropToViewport: expose("cropToViewport"), }), - ...(expose('ignoreSelectors') && { - ignoreSelectors: expose('ignoreSelectors'), + ...(expose("ignoreSelectors") && { + ignoreSelectors: expose("ignoreSelectors"), }), }); // these client-side lifecycle hooks will be added to the user's Cypress suite beforeEach(() => { // don't take snapshots when running `cypress open` - if (!Cypress.config('isTextTerminal')) { + if (!Cypress.config("isTextTerminal")) { return; } // this "manualSnapshots" variable will be available before, during, and after the test, // then cleaned up before the next test is run // (see https://docs.cypress.io/guides/core-concepts/variables-and-aliases#Aliases) - cy.wrap([]).as('manualSnapshots'); - cy.task('prepareArchives', { - action: 'setup-network-listener', - payload: { allowedDomains: Cypress.expose('assetDomains') }, + cy.wrap([]).as("manualSnapshots"); + cy.task("prepareArchives", { + action: "setup-network-listener", + payload: { allowedDomains: Cypress.expose("assetDomains") }, }); }); afterEach(() => { // don't take snapshots when running `cypress open` - if (!Cypress.config('isTextTerminal')) { + if (!Cypress.config("isTextTerminal")) { return; } cy.window().then((win) => { const viewport = { width: win.innerWidth, height: win.innerHeight }; - const colorScheme = win.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const colorScheme = win.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; // can we be sure this always fires after all the requests are back? cy.document().then((doc) => { cy.wrap(takeSnapshot(doc, viewport, colorScheme)).then( (automaticSnapshot: CypressSnapshot) => { - cy.get('@manualSnapshots').then((manualSnapshots = []) => { + cy.get("@manualSnapshots").then((manualSnapshots = []) => { cy.url().then((url) => { // pass the snapshot to the server to write to disk - cy.task('prepareArchives', { - action: 'save-archives', + cy.task("prepareArchives", { + action: "save-archives", payload: { testTitlePath: [ // @ts-expect-error relativeToCommonRoot is on spec (but undocumented) @@ -72,14 +72,14 @@ afterEach(() => { ...manualSnapshots, ...(automaticSnapshot ? [automaticSnapshot] : []), ], - chromaticStorybookParams: buildChromaticParams(Cypress.expose), + chromaticStorybookParams: buildChromaticParams(Cypress.expose.bind(Cypress)), pageUrl: url, - outputDir: Cypress.config('downloadsFolder'), + outputDir: Cypress.config("downloadsFolder"), }, }); }); }); - } + }, ); }); }); diff --git a/packages/cypress/src/takeSnapshot.ts b/packages/cypress/src/takeSnapshot.ts index f33ecbeb..9be3a586 100644 --- a/packages/cypress/src/takeSnapshot.ts +++ b/packages/cypress/src/takeSnapshot.ts @@ -1,24 +1,25 @@ -import { createMirror, snapshot } from '@chromaui/rrweb-snapshot'; -import { CypressSnapshot } from './types'; -import type { serializedNodeWithId } from '@rrweb/types'; +import { createMirror, snapshot } from "@chromaui/rrweb-snapshot"; +import type { serializedNodeWithId } from "@rrweb/types"; + +import { CypressSnapshot } from "./types"; export const takeSnapshot = ( doc: Document, viewport: { width: number; height: number }, - colorScheme: 'light' | 'dark', - isManualSnapshot?: boolean + colorScheme: "light" | "dark", + isManualSnapshot?: boolean, ): Promise => { return new Promise((resolve) => { - if (!isManualSnapshot && Cypress.expose('disableAutoSnapshot')) { + if (!isManualSnapshot && Cypress.expose("disableAutoSnapshot")) { resolve(null); } const mirror = createMirror(); const domSnapshot = snapshot(doc, { recordCanvas: true, mirror })!; - const pseudoClassIds: CypressSnapshot['pseudoClassIds'] = {}; + const pseudoClassIds: CypressSnapshot["pseudoClassIds"] = {}; - for (const className of [':hover', ':focus', ':focus-visible', ':active'] as const) { + for (const className of [":hover", ":focus", ":focus-visible", ":active"] as const) { const elements = doc.querySelectorAll(className); const ids = Array.from(elements, (el) => mirror.getId(el)).filter((id) => id !== -1); pseudoClassIds[className] = ids; @@ -42,7 +43,7 @@ export const takeSnapshot = ( await Promise.all( // @ts-expect-error we assume childNodes will be on there node.childNodes.map(async (childNode) => { - if (childNode.tagName === 'img' && childNode.attributes.src?.startsWith('blob:')) { + if (childNode.tagName === "img" && childNode.attributes.src?.startsWith("blob:")) { const base64Url = await toDataURL(childNode.attributes.src); childNode.attributes.src = base64Url; } @@ -50,11 +51,11 @@ export const takeSnapshot = ( if (childNode.childNodes?.length) { await replaceBlobUrls(childNode); } - }) + }), ); }; - replaceBlobUrls(domSnapshot).then(() => { + void replaceBlobUrls(domSnapshot).then(() => { resolve({ snapshot: domSnapshot, viewport, colorScheme, pseudoClassIds }); }); }); diff --git a/packages/cypress/src/types.test-d.ts b/packages/cypress/src/types.test-d.ts index 7b39c514..c2afa7d8 100644 --- a/packages/cypress/src/types.test-d.ts +++ b/packages/cypress/src/types.test-d.ts @@ -1,8 +1,8 @@ -import { expectTypeOf, test } from 'vitest'; +import { expectTypeOf, test } from "vitest"; -import '../dist/support.mjs'; +import "../dist/support.mjs"; -test('takeSnapshot', () => { +test("takeSnapshot", () => { expectTypeOf(cy.takeSnapshot).toBeFunction(); expectTypeOf(cy.takeSnapshot).parameter(0).toEqualTypeOf(); diff --git a/packages/cypress/src/types.ts b/packages/cypress/src/types.ts index 8ddd555b..7a5c196a 100644 --- a/packages/cypress/src/types.ts +++ b/packages/cypress/src/types.ts @@ -1,12 +1,12 @@ -import type { serializedNodeWithId } from '@rrweb/types'; -import type { DOMSnapshots } from '@chromatic-com/shared-e2e'; +import type { DOMSnapshots } from "@chromatic-com/shared-e2e"; +import type { serializedNodeWithId } from "@rrweb/types"; export interface CypressSnapshot { // the name of the snapshot (optionally provided for manual snapshots, never provided for automatic snapshots) name?: string; // the DOM snapshot snapshot: serializedNodeWithId; - viewport: DOMSnapshots[string]['viewport']; - colorScheme: DOMSnapshots[string]['colorScheme']; - pseudoClassIds: DOMSnapshots[string]['pseudoClassIds']; + viewport: DOMSnapshots[string]["viewport"]; + colorScheme: DOMSnapshots[string]["colorScheme"]; + pseudoClassIds: DOMSnapshots[string]["pseudoClassIds"]; } diff --git a/packages/cypress/tests/cypress.config.ts b/packages/cypress/tests/cypress.config.ts index d3a8a633..be8a5e73 100644 --- a/packages/cypress/tests/cypress.config.ts +++ b/packages/cypress/tests/cypress.config.ts @@ -1,19 +1,21 @@ -import { defineConfig } from 'cypress'; -import { installPlugin } from '../dist/index.mjs'; -import { existsSync } from 'node:fs'; +import { existsSync } from "node:fs"; + +import { defineConfig } from "cypress"; + +import { installPlugin } from "../dist/index.mjs"; export default defineConfig({ // `downloadsFolder` cannot be overridden in tests, so we're setting // this to a non-default value for asserting in the tests - downloadsFolder: 'cypress/test-downloads', + downloadsFolder: "cypress/test-downloads", // needed since we use common mock images between Cypress and Playwright - fixturesFolder: '../../../test-server/fixtures', + fixturesFolder: "../../../test-server/fixtures", screenshotOnRunFailure: false, e2e: { - baseUrl: 'http://localhost:3000', + baseUrl: "http://localhost:3000", setupNodeEvents(on, config) { installPlugin(on, config); - on('task', { + on("task", { directoryExists(directoryName) { return existsSync(directoryName); }, diff --git a/packages/cypress/tests/cypress/e2e/archiving-assets.cy.ts b/packages/cypress/tests/cypress/e2e/archiving-assets.cy.ts index 5382a443..ed3cf32c 100644 --- a/packages/cypress/tests/cypress/e2e/archiving-assets.cy.ts +++ b/packages/cypress/tests/cypress/e2e/archiving-assets.cy.ts @@ -1,68 +1,68 @@ -it('query params determine which asset is served', () => { - cy.visit('/asset-paths/query-params'); +it("query params determine which asset is served", () => { + cy.visit("/asset-paths/query-params"); }); -it('asset doesnt prevent directory from being created', () => { - cy.visit('/asset-paths/asset-at-directory-name'); +it("asset doesnt prevent directory from being created", () => { + cy.visit("/asset-paths/asset-at-directory-name"); }); -it('asset is found at relative path', () => { - cy.visit('/asset-paths/relative-path'); +it("asset is found at relative path", () => { + cy.visit("/asset-paths/relative-path"); }); -it('long file names are allowed', () => { - cy.visit('/asset-paths/long-file-name'); +it("long file names are allowed", () => { + cy.visit("/asset-paths/long-file-name"); }); -it('external asset is not archived (but still renders)', () => { - cy.visit('/asset-paths/external-asset-not-archived'); +it("external asset is not archived (but still renders)", () => { + cy.visit("/asset-paths/external-asset-not-archived"); }); // TODO: Unskip when Cypress support achieves parity with Playwright -it.skip('external asset is archived', () => { +it.skip("external asset is archived", () => { // mock the external image (which we'll archive) - cy.intercept('https://some.external/domain/image.png', { fixture: 'pink.png' }); + cy.intercept("https://some.external/domain/image.png", { fixture: "pink.png" }); - cy.visit('/asset-paths/external-asset-archived'); + cy.visit("/asset-paths/external-asset-archived"); }); -it('assets from css urls are archived', () => { - cy.visit('/asset-paths/css-urls'); - cy.get('.with-background-img').should('be.visible'); +it("assets from css urls are archived", () => { + cy.visit("/asset-paths/css-urls"); + cy.get(".with-background-img").should("be.visible"); }); -it('assets from relative css urls with base tag are archived', () => { - cy.visit('/asset-paths/relative-urls-with-base'); +it("assets from relative css urls with base tag are archived", () => { + cy.visit("/asset-paths/relative-urls-with-base"); }); -it('assets from data urls are archived', () => { - cy.visit('/asset-paths/data-urls'); +it("assets from data urls are archived", () => { + cy.visit("/asset-paths/data-urls"); }); // TODO: Unskip when Cypress support achieves parity with Playwright -it.skip('percents in URLs are handled', () => { - cy.visit('/asset-paths/percents'); +it.skip("percents in URLs are handled", () => { + cy.visit("/asset-paths/percents"); }); -it('colons in URLs are handled', () => { - cy.visit('/asset-paths/colons'); +it("colons in URLs are handled", () => { + cy.visit("/asset-paths/colons"); }); -it('srcset is used to determine image asset URL', () => { - cy.visit('/asset-paths/srcset'); +it("srcset is used to determine image asset URL", () => { + cy.visit("/asset-paths/srcset"); }); -it('picture source is captured, multiple source elements', () => { - cy.visit('/asset-paths/picture'); +it("picture source is captured, multiple source elements", () => { + cy.visit("/asset-paths/picture"); }); // TODO: Unskip when we use one-archive-per-test in Cypress (like we do in Playwright) // currently, we use one archive for all the tests, but in this case it means we capture // the wrong image (since the unmatching images are already in the global archive, we // mistakenly assume they were "captured" by this test and use them instead of the fallback image) -context.skip('mobile', { viewportWidth: 500, viewportHeight: 500 }, () => { - it('picture source is captured, multiple source elements', () => { - cy.visit('/asset-paths/picture'); +context.skip("mobile", { viewportWidth: 500, viewportHeight: 500 }, () => { + it("picture source is captured, multiple source elements", () => { + cy.visit("/asset-paths/picture"); }); }); @@ -70,36 +70,36 @@ context.skip('mobile', { viewportWidth: 500, viewportHeight: 500 }, () => { // currently, we use one archive for all the tests, but in this case it means we capture // the wrong image (since the unmatching images are already in the global archive, we // mistakenly assume they were "captured" by this test and use them instead of the fallback image) -it.skip('picture captures fallback image', () => { - cy.visit('/asset-paths/picture-no-matching-source'); +it.skip("picture captures fallback image", () => { + cy.visit("/asset-paths/picture-no-matching-source"); }); -it('picture source is captured, single source with srcset', () => { - cy.visit('/asset-paths/picture-multiple-srcset'); +it("picture source is captured, single source with srcset", () => { + cy.visit("/asset-paths/picture-multiple-srcset"); }); // TODO: Unskip when we use one-archive-per-test in Cypress (like we do in Playwright) // currently, we use one archive for all the tests, but in this case it means we capture // the wrong image (since the unmatching images are already in the global archive, we // mistakenly assume they were "captured" by this test and use them instead of the fallback image) -context.skip('mobile', { viewportWidth: 500, viewportHeight: 500 }, () => { - it('picture source is captured, single source with srcset', () => { - cy.visit('/asset-paths/picture-multiple-srcset'); +context.skip("mobile", { viewportWidth: 500, viewportHeight: 500 }, () => { + it("picture source is captured, single source with srcset", () => { + cy.visit("/asset-paths/picture-multiple-srcset"); }); }); -it('external CSS files are inlined', () => { - cy.visit('/asset-paths/external-css-files'); +it("external CSS files are inlined", () => { + cy.visit("/asset-paths/external-css-files"); }); -it('video poster image is rendered', () => { - cy.visit('/asset-paths/video-poster'); +it("video poster image is rendered", () => { + cy.visit("/asset-paths/video-poster"); }); -it('link tags for fonts preloads and other things are handled', () => { - cy.visit('/asset-paths/link-tags'); +it("link tags for fonts preloads and other things are handled", () => { + cy.visit("/asset-paths/link-tags"); }); -it('use tags for sprites are archived', () => { - cy.visit('/asset-paths/sprites'); +it("use tags for sprites are archived", () => { + cy.visit("/asset-paths/sprites"); }); diff --git a/packages/cypress/tests/cypress/e2e/auth.cy.ts b/packages/cypress/tests/cypress/e2e/auth.cy.ts index 8cfccd26..311073b7 100644 --- a/packages/cypress/tests/cypress/e2e/auth.cy.ts +++ b/packages/cypress/tests/cypress/e2e/auth.cy.ts @@ -1,9 +1,9 @@ -it('can succeed with basic authentication using locally defined credentials', () => { - cy.visit('/protected', { +it("can succeed with basic authentication using locally defined credentials", () => { + cy.visit("/protected", { auth: { - username: 'user', - password: 'secret', + username: "user", + password: "secret", }, }); - cy.contains('I AM PROTECTED!!!').should('be.visible'); + cy.contains("I AM PROTECTED!!!").should("be.visible"); }); diff --git a/packages/cypress/tests/cypress/e2e/blob-urls.cy.ts b/packages/cypress/tests/cypress/e2e/blob-urls.cy.ts index 2db524b8..fe3ad846 100644 --- a/packages/cypress/tests/cypress/e2e/blob-urls.cy.ts +++ b/packages/cypress/tests/cypress/e2e/blob-urls.cy.ts @@ -1,18 +1,18 @@ -it('Upload a Single file and Assert blob', { expose: { ignoreSelectors: ['#objectUrl'] } }, () => { - cy.visit('/createObjectUrl'); +it("Upload a Single file and Assert blob", { expose: { ignoreSelectors: ["#objectUrl"] } }, () => { + cy.visit("/createObjectUrl"); - cy.get('#fileInput').selectFile('../../../test-server/fixtures/blue.png'); + cy.get("#fileInput").selectFile("../../../test-server/fixtures/blue.png"); - cy.get('#objectUrl') - .invoke('text') - .should('match', /blob:.*/); + cy.get("#objectUrl") + .invoke("text") + .should("match", /blob:.*/); }); -it('Fetch data for blob', () => { - cy.visit('/createObjectUrl?noUpload=true'); +it("Fetch data for blob", () => { + cy.visit("/createObjectUrl?noUpload=true"); - cy.get('#blobImg') - .should('be.visible') + cy.get("#blobImg") + .should("be.visible") .should(($img) => { // @ts-expect-error -- "naturalWidth" and "naturalHeight" are set when the image loads expect($img[0].naturalWidth).to.eq(10); @@ -20,18 +20,18 @@ it('Fetch data for blob', () => { }); it( - 'Captures blob contents for manual snapshots', - { expose: { ignoreSelectors: ['#objectUrl'] } }, + "Captures blob contents for manual snapshots", + { expose: { ignoreSelectors: ["#objectUrl"] } }, () => { - cy.visit('/createObjectUrl'); + cy.visit("/createObjectUrl"); - cy.get('#fileInput').selectFile('../../../test-server/fixtures/blue.png'); + cy.get("#fileInput").selectFile("../../../test-server/fixtures/blue.png"); - cy.get('#objectUrl') - .invoke('text') - .should('match', /blob:.*/); - cy.takeSnapshot('Manual snapshot (Should show a blue square)'); + cy.get("#objectUrl") + .invoke("text") + .should("match", /blob:.*/); + cy.takeSnapshot("Manual snapshot (Should show a blue square)"); - cy.visit('/asset-paths/query-params'); - } + cy.visit("/asset-paths/query-params"); + }, ); diff --git a/packages/cypress/tests/cypress/e2e/canvas.cy.ts b/packages/cypress/tests/cypress/e2e/canvas.cy.ts index 8a794b48..0781a64a 100644 --- a/packages/cypress/tests/cypress/e2e/canvas.cy.ts +++ b/packages/cypress/tests/cypress/e2e/canvas.cy.ts @@ -1,3 +1,3 @@ -it('captures basic canvas elements', () => { - cy.visit('/canvas'); +it("captures basic canvas elements", () => { + cy.visit("/canvas"); }); diff --git a/packages/cypress/tests/cypress/e2e/color-scheme.cy.ts b/packages/cypress/tests/cypress/e2e/color-scheme.cy.ts index d33752ca..bcf7e024 100644 --- a/packages/cypress/tests/cypress/e2e/color-scheme.cy.ts +++ b/packages/cypress/tests/cypress/e2e/color-scheme.cy.ts @@ -1,51 +1,51 @@ afterEach(() => { // Reset the emulation so it doesn't leak into other spec files cy.wrap(null).then(() => - Cypress.automation('remote:debugger:protocol', { - command: 'Emulation.setEmulatedMedia', + Cypress.automation("remote:debugger:protocol", { + command: "Emulation.setEmulatedMedia", params: { features: [] }, - }) + }), ); }); -describe('light color scheme', () => { - it('renders the light-only content', () => { - cy.wrap(null).then(() => setColorScheme('light')); - cy.visit('/color-scheme'); +describe("light color scheme", () => { + it("renders the light-only content", () => { + cy.wrap(null).then(() => setColorScheme("light")); + cy.visit("/color-scheme"); - cy.contains('Only visible in LIGHT mode').should('be.visible'); - cy.contains('Only visible in DARK mode').should('not.be.visible'); + cy.contains("Only visible in LIGHT mode").should("be.visible"); + cy.contains("Only visible in DARK mode").should("not.be.visible"); }); }); -describe('dark color scheme', () => { - it('renders the dark-only content', () => { - cy.wrap(null).then(() => setColorScheme('dark')); - cy.visit('/color-scheme'); +describe("dark color scheme", () => { + it("renders the dark-only content", () => { + cy.wrap(null).then(() => setColorScheme("dark")); + cy.visit("/color-scheme"); - cy.contains('Only visible in DARK mode').should('be.visible'); - cy.contains('Only visible in LIGHT mode').should('not.be.visible'); + cy.contains("Only visible in DARK mode").should("be.visible"); + cy.contains("Only visible in LIGHT mode").should("not.be.visible"); }); }); it( - 'captures both color schemes inside a single test case', + "captures both color schemes inside a single test case", { expose: { disableAutoSnapshot: true } }, () => { - cy.wrap(null).then(() => setColorScheme('dark')); - cy.visit('/color-scheme'); - cy.contains('Only visible in DARK mode').should('be.visible'); - cy.takeSnapshot('dark'); + cy.wrap(null).then(() => setColorScheme("dark")); + cy.visit("/color-scheme"); + cy.contains("Only visible in DARK mode").should("be.visible"); + cy.takeSnapshot("dark"); - cy.wrap(null).then(() => setColorScheme('light')); - cy.contains('Only visible in LIGHT mode').should('be.visible'); - cy.takeSnapshot('light'); - } + cy.wrap(null).then(() => setColorScheme("light")); + cy.contains("Only visible in LIGHT mode").should("be.visible"); + cy.takeSnapshot("light"); + }, ); -function setColorScheme(value: 'light' | 'dark') { - return Cypress.automation('remote:debugger:protocol', { - command: 'Emulation.setEmulatedMedia', - params: { features: [{ name: 'prefers-color-scheme', value }] }, +function setColorScheme(value: "light" | "dark") { + return Cypress.automation("remote:debugger:protocol", { + command: "Emulation.setEmulatedMedia", + params: { features: [{ name: "prefers-color-scheme", value }] }, }); } diff --git a/packages/cypress/tests/cypress/e2e/constructable-stylesheets.cy.ts b/packages/cypress/tests/cypress/e2e/constructable-stylesheets.cy.ts index 3f248bc8..b92f7e22 100644 --- a/packages/cypress/tests/cypress/e2e/constructable-stylesheets.cy.ts +++ b/packages/cypress/tests/cypress/e2e/constructable-stylesheets.cy.ts @@ -1,15 +1,15 @@ -it('styles render in plain html', () => { - cy.visit('/constructable-stylesheets/plain'); +it("styles render in plain html", () => { + cy.visit("/constructable-stylesheets/plain"); }); -it('styles render in shadow dom elements', () => { - cy.visit('/constructable-stylesheets/shadow-dom'); +it("styles render in shadow dom elements", () => { + cy.visit("/constructable-stylesheets/shadow-dom"); }); -it('styles render in web components', () => { - cy.visit('/constructable-stylesheets/web-components'); +it("styles render in web components", () => { + cy.visit("/constructable-stylesheets/web-components"); }); -it('styles render in web components in shadow dom', () => { - cy.visit('/constructable-stylesheets/web-components-shadow-dom'); +it("styles render in web components in shadow dom", () => { + cy.visit("/constructable-stylesheets/web-components-shadow-dom"); }); diff --git a/packages/cypress/tests/cypress/e2e/css-pseudo-states.cy.ts b/packages/cypress/tests/cypress/e2e/css-pseudo-states.cy.ts index 71515f09..d054b960 100644 --- a/packages/cypress/tests/cypress/e2e/css-pseudo-states.cy.ts +++ b/packages/cypress/tests/cypress/e2e/css-pseudo-states.cy.ts @@ -1,57 +1,57 @@ type Point = { x: number; y: number }; -it('captures :hover state', { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/css-pseudo-states'); - withElementCenter('button#target', mouseMove); +it("captures :hover state", { expose: { disableAutoSnapshot: true } }, () => { + cy.visit("/css-pseudo-states"); + withElementCenter("button#target", mouseMove); - cy.get('button#target:hover').should('exist'); - cy.takeSnapshot('hover'); + cy.get("button#target:hover").should("exist"); + cy.takeSnapshot("hover"); }); -it('captures :focus state', { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/css-pseudo-states'); +it("captures :focus state", { expose: { disableAutoSnapshot: true } }, () => { + cy.visit("/css-pseudo-states"); - withElementCenter('button#target', (point) => + withElementCenter("button#target", (point) => mouseMove(point) .then(() => mouseDown(point)) - .then(() => mouseUp(point)) + .then(() => mouseUp(point)), ); - cy.get('button#target:focus').should('exist'); - cy.takeSnapshot('focus'); + cy.get("button#target:focus").should("exist"); + cy.takeSnapshot("focus"); }); -it('captures :active state', { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/css-pseudo-states'); +it("captures :active state", { expose: { disableAutoSnapshot: true } }, () => { + cy.visit("/css-pseudo-states"); let pressedCoords: Point = { x: 0, y: 0 }; - withElementCenter('button#target', (point) => + withElementCenter("button#target", (point) => mouseMove(point) .then(() => mouseDown(point)) .then(() => { pressedCoords = point; - }) + }), ); - cy.get('button#target:active').should('exist'); - cy.takeSnapshot('active'); + cy.get("button#target:active").should("exist"); + cy.takeSnapshot("active"); cy.then(() => mouseUp(pressedCoords)); }); -it('captures :focus-visible state', { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/css-pseudo-states'); - cy.get('button#tab-cycle').focus(); +it("captures :focus-visible state", { expose: { disableAutoSnapshot: true } }, () => { + cy.visit("/css-pseudo-states"); + cy.get("button#tab-cycle").focus(); cy.wrap(null).then(keyboardTab); - cy.get('button#target:focus-visible').should('exist'); - cy.takeSnapshot('focus-visible'); + cy.get("button#target:focus-visible").should("exist"); + cy.takeSnapshot("focus-visible"); }); function cdp(command: string, params: Record) { - return Cypress.automation('remote:debugger:protocol', { command, params }); + return Cypress.automation("remote:debugger:protocol", { command, params }); } function getCdpElementCenter(el: HTMLElement): Point { @@ -65,8 +65,8 @@ function getCdpElementCenter(el: HTMLElement): Point { }; } - const autIframe = [...autWindow.parent.document.querySelectorAll('iframe')].find( - (frame) => frame.contentWindow === autWindow + const autIframe = [...autWindow.parent.document.querySelectorAll("iframe")].find( + (frame) => frame.contentWindow === autWindow, ); if (!autIframe) { @@ -90,51 +90,51 @@ function withElementCenter(selector: string, cb: (point: Point) => Promise - cdp('Input.dispatchKeyEvent', { - type: 'keyUp', + cdp("Input.dispatchKeyEvent", { + type: "keyUp", windowsVirtualKeyCode: 9, - key: 'Tab', - code: 'Tab', - }) + key: "Tab", + code: "Tab", + }), ); } diff --git a/packages/cypress/tests/cypress/e2e/custom-downloads-directory.cy.ts b/packages/cypress/tests/cypress/e2e/custom-downloads-directory.cy.ts index 399e96ac..383b8a51 100644 --- a/packages/cypress/tests/cypress/e2e/custom-downloads-directory.cy.ts +++ b/packages/cypress/tests/cypress/e2e/custom-downloads-directory.cy.ts @@ -1,13 +1,13 @@ // Snapshots are disabled because we're asserting on file system concerns, // not testing anything visual it( - 'downloads archives to the user-specified folder', + "downloads archives to the user-specified folder", { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/asset-paths/query-params'); - const chromaticArchivesDir = `${Cypress.config('downloadsFolder')}/chromatic-archives`; - cy.task('directoryExists', chromaticArchivesDir).then((dirExists) => { + cy.visit("/asset-paths/query-params"); + const chromaticArchivesDir = `${Cypress.config("downloadsFolder")}/chromatic-archives`; + cy.task("directoryExists", chromaticArchivesDir).then((dirExists) => { expect(dirExists).to.be.true; }); - } + }, ); diff --git a/packages/cypress/tests/cypress/e2e/duplicate-test-names.cy.ts b/packages/cypress/tests/cypress/e2e/duplicate-test-names.cy.ts index 2aa5d51f..820a0207 100644 --- a/packages/cypress/tests/cypress/e2e/duplicate-test-names.cy.ts +++ b/packages/cypress/tests/cypress/e2e/duplicate-test-names.cy.ts @@ -1,16 +1,16 @@ -describe('duplicate test names', () => { - it('example', () => { - cy.visit('/'); +describe("duplicate test names", () => { + it("example", () => { + cy.visit("/"); }); - it('example', () => { - cy.visit('/'); + it("example", () => { + cy.visit("/"); }); }); -it('duplicate snapshot names', { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/'); +it("duplicate snapshot names", { expose: { disableAutoSnapshot: true } }, () => { + cy.visit("/"); - cy.takeSnapshot('example'); - cy.takeSnapshot('example'); + cy.takeSnapshot("example"); + cy.takeSnapshot("example"); }); diff --git a/packages/cypress/tests/cypress/e2e/embeds.cy.ts b/packages/cypress/tests/cypress/e2e/embeds.cy.ts index 38031d7a..28e8ff0d 100644 --- a/packages/cypress/tests/cypress/e2e/embeds.cy.ts +++ b/packages/cypress/tests/cypress/e2e/embeds.cy.ts @@ -1,60 +1,64 @@ -it('same-origin embed page loads', () => { - cy.visit('/embeds/same-origin'); - cy.contains('h1', 'Embeds').should('be.visible'); - cy.get('iframe[title="Same-origin iframe"]').should('be.visible'); +it("same-origin embed page loads", () => { + cy.visit("/embeds/same-origin"); + cy.contains("h1", "Embeds").should("be.visible"); + cy.get('iframe[title="Same-origin iframe"]').should("be.visible"); }); -it('cross-origin embed page loads', () => { - cy.visit('/embeds/cross-origin'); - cy.contains('h1', 'Embeds').should('be.visible'); +it("cross-origin embed page loads", () => { + cy.visit("/embeds/cross-origin"); + cy.contains("h1", "Embeds").should("be.visible"); // As we don't use --disable-web-security, we cannot query the cross-origin iframes contents. // Verify that their content is expected with separate fetch. - cy.get('iframe[title="Cross-origin iframe"]').should('be.visible'); - cy.request('http://localhost:3001/').its('body').should('include', 'Embedded page'); + cy.get('iframe[title="Cross-origin iframe"]').should("be.visible"); + cy.request("http://localhost:3001/").its("body").should("include", "Embedded page"); }); -it('embedded page background color can be changed', { expose: { disableAutoSnapshot: true } }, () => { - /** Re-query each time; wait until iframe document + body exist (load can lag after parent paint). */ - const inEmbeddedBody = (fn: () => void) => { - cy.get('iframe[title="Same-origin iframe"]') - .should(($iframe) => { - const el = $iframe[0] as HTMLIFrameElement; - const body = el.contentDocument?.body; - expect(body, 'iframe body').to.not.equal(null); - expect(body!.querySelector('h1')?.textContent).to.include('Embedded page'); - }) - .then(($iframe) => { - const body = ($iframe[0] as HTMLIFrameElement).contentDocument!.body; - cy.wrap(body).within(fn); - }); - }; - - cy.visit('/embeds/same-origin'); - cy.contains('h1', 'Embeds').should('be.visible'); - - inEmbeddedBody(() => { - cy.contains('h1', 'Embedded page').should('be.visible'); - cy.contains('button', 'Red').click(); - cy.root().should('have.css', 'background-color', 'rgb(255, 0, 0)'); - }); - cy.takeSnapshot('Red background'); - - inEmbeddedBody(() => { - cy.contains('button', 'Yellow').click(); - cy.root().should('have.css', 'background-color', 'rgb(255, 255, 0)'); - }); - cy.takeSnapshot('Yellow background'); - - inEmbeddedBody(() => { - cy.contains('button', 'Blue').click(); - cy.root().should('have.css', 'background-color', 'rgb(0, 0, 255)'); - }); - cy.takeSnapshot('Blue background'); - - inEmbeddedBody(() => { - cy.contains('button', 'Reset').click(); - cy.root().should('have.css', 'background-color', 'rgba(0, 0, 0, 0)'); - }); - cy.takeSnapshot('Reset background'); -}); +it( + "embedded page background color can be changed", + { expose: { disableAutoSnapshot: true } }, + () => { + /** Re-query each time; wait until iframe document + body exist (load can lag after parent paint). */ + const inEmbeddedBody = (fn: () => void) => { + cy.get('iframe[title="Same-origin iframe"]') + .should(($iframe) => { + const el = $iframe[0] as HTMLIFrameElement; + const body = el.contentDocument?.body; + expect(body, "iframe body").to.not.equal(null); + expect(body!.querySelector("h1")?.textContent).to.include("Embedded page"); + }) + .then(($iframe) => { + const body = ($iframe[0] as HTMLIFrameElement).contentDocument!.body; + cy.wrap(body).within(fn); + }); + }; + + cy.visit("/embeds/same-origin"); + cy.contains("h1", "Embeds").should("be.visible"); + + inEmbeddedBody(() => { + cy.contains("h1", "Embedded page").should("be.visible"); + cy.contains("button", "Red").click(); + cy.root().should("have.css", "background-color", "rgb(255, 0, 0)"); + }); + cy.takeSnapshot("Red background"); + + inEmbeddedBody(() => { + cy.contains("button", "Yellow").click(); + cy.root().should("have.css", "background-color", "rgb(255, 255, 0)"); + }); + cy.takeSnapshot("Yellow background"); + + inEmbeddedBody(() => { + cy.contains("button", "Blue").click(); + cy.root().should("have.css", "background-color", "rgb(0, 0, 255)"); + }); + cy.takeSnapshot("Blue background"); + + inEmbeddedBody(() => { + cy.contains("button", "Reset").click(); + cy.root().should("have.css", "background-color", "rgba(0, 0, 0, 0)"); + }); + cy.takeSnapshot("Reset background"); + }, +); diff --git a/packages/cypress/tests/cypress/e2e/forms.cy.ts b/packages/cypress/tests/cypress/e2e/forms.cy.ts index f67773e8..5c2b5a3f 100644 --- a/packages/cypress/tests/cypress/e2e/forms.cy.ts +++ b/packages/cypress/tests/cypress/e2e/forms.cy.ts @@ -1,5 +1,5 @@ -it('form submits successfully', () => { - cy.visit('/forms'); - cy.contains('Click me').click(); - cy.contains('OK!').should('be.visible'); +it("form submits successfully", () => { + cy.visit("/forms"); + cy.contains("Click me").click(); + cy.contains("OK!").should("be.visible"); }); diff --git a/packages/cypress/tests/cypress/e2e/ignored-regions.cy.ts b/packages/cypress/tests/cypress/e2e/ignored-regions.cy.ts index b88cacd8..e75f93b3 100644 --- a/packages/cypress/tests/cypress/e2e/ignored-regions.cy.ts +++ b/packages/cypress/tests/cypress/e2e/ignored-regions.cy.ts @@ -1,6 +1,10 @@ // NOTE: This is a test that is meant to be run through Chromatic, so it doesn't actually work // with the automated test suite. -it('ignored regions work with chromatic', { expose: { ignoreSelectors: ['.custom-ignore'] } }, () => { - cy.visit('/ignore'); - cy.wait(1000); -}); +it( + "ignored regions work with chromatic", + { expose: { ignoreSelectors: [".custom-ignore"] } }, + () => { + cy.visit("/ignore"); + cy.wait(1000); + }, +); diff --git a/packages/cypress/tests/cypress/e2e/long-test-names.cy.ts b/packages/cypress/tests/cypress/e2e/long-test-names.cy.ts index 34c9ac2c..22006593 100644 --- a/packages/cypress/tests/cypress/e2e/long-test-names.cy.ts +++ b/packages/cypress/tests/cypress/e2e/long-test-names.cy.ts @@ -1,13 +1,13 @@ -describe('this is a very long story name it just keeps going and going and it cannot stop and it will not stop ba bada da da dum dum dum', () => { - it('and this is also an incredibly long test name because there are just a bunch of random chars at the end like this ldlk elke lekj felk felkf lkf lsf lkef lse flskef ls fls eflsj flksef', () => { - cy.visit('/'); +describe("this is a very long story name it just keeps going and going and it cannot stop and it will not stop ba bada da da dum dum dum", () => { + it("and this is also an incredibly long test name because there are just a bunch of random chars at the end like this ldlk elke lekj felk felkf lkf lsf lkef lse flskef ls fls eflsj flksef", () => { + cy.visit("/"); }); - it('and this is also an incredibly long test name because there are just a bunch of random chars at the end like this ldlk elke lekj felk felkf lkf lsf lkef lse flskef ls fls eflsj flksef 2', () => { - cy.visit('/'); + it("and this is also an incredibly long test name because there are just a bunch of random chars at the end like this ldlk elke lekj felk felkf lkf lsf lkef lse flskef ls fls eflsj flksef 2", () => { + cy.visit("/"); }); - it('multi-byte characters test case: ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ', () => { - cy.visit('/'); + it("multi-byte characters test case: ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ", () => { + cy.visit("/"); }); }); diff --git a/packages/cypress/tests/cypress/e2e/manual-snapshots.cy.ts b/packages/cypress/tests/cypress/e2e/manual-snapshots.cy.ts index 0e5c8d6a..3cfb3d7a 100644 --- a/packages/cypress/tests/cypress/e2e/manual-snapshots.cy.ts +++ b/packages/cypress/tests/cypress/e2e/manual-snapshots.cy.ts @@ -1,20 +1,20 @@ -it('multiple snapshots are taken', () => { - cy.visit('/manual-snapshots'); +it("multiple snapshots are taken", () => { + cy.visit("/manual-snapshots"); // manual snapshot with name - cy.takeSnapshot('accordion collapsed'); + cy.takeSnapshot("accordion collapsed"); cy.contains("I'm an accordion, click me!").click(); // manual snapshot without name cy.takeSnapshot(); cy.contains("I'm an accordion, click me!").click(); - cy.get('details').should('not.have.attr', 'open'); + cy.get("details").should("not.have.attr", "open"); }); it( - 'manual snapshot is taken even when automatic snapshots are turned off', + "manual snapshot is taken even when automatic snapshots are turned off", { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/manual-snapshots'); + cy.visit("/manual-snapshots"); cy.contains("I'm an accordion, click me!").click(); - cy.takeSnapshot('Manual snapshot without automatic snapshot'); - } + cy.takeSnapshot("Manual snapshot without automatic snapshot"); + }, ); diff --git a/packages/cypress/tests/cypress/e2e/newline-test-names.cy.ts b/packages/cypress/tests/cypress/e2e/newline-test-names.cy.ts index d7929162..bef5c155 100644 --- a/packages/cypress/tests/cypress/e2e/newline-test-names.cy.ts +++ b/packages/cypress/tests/cypress/e2e/newline-test-names.cy.ts @@ -3,12 +3,12 @@ Test name newlines `, () => { - it('Are\n\rRemoved\r\nFrom\nFile\rNames\n\n\r\r', () => { - cy.visit('/'); + it("Are\n\rRemoved\r\nFrom\nFile\rNames\n\n\r\r", () => { + cy.visit("/"); }); - it('newlines in snapshot name', { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/'); - cy.takeSnapshot('snapshot name\nwith newlines\r\nand carriage returns'); + it("newlines in snapshot name", { expose: { disableAutoSnapshot: true } }, () => { + cy.visit("/"); + cy.takeSnapshot("snapshot name\nwith newlines\r\nand carriage returns"); }); }); diff --git a/packages/cypress/tests/cypress/e2e/no-doctype.cy.ts b/packages/cypress/tests/cypress/e2e/no-doctype.cy.ts index b1558278..736c70b1 100644 --- a/packages/cypress/tests/cypress/e2e/no-doctype.cy.ts +++ b/packages/cypress/tests/cypress/e2e/no-doctype.cy.ts @@ -1,3 +1,3 @@ -it('pages without a doctype are archived', () => { - cy.visit('/no-doctype'); +it("pages without a doctype are archived", () => { + cy.visit("/no-doctype"); }); diff --git a/packages/cypress/tests/cypress/e2e/options.cy.ts b/packages/cypress/tests/cypress/e2e/options.cy.ts index bd0b4fcf..bd39818c 100644 --- a/packages/cypress/tests/cypress/e2e/options.cy.ts +++ b/packages/cypress/tests/cypress/e2e/options.cy.ts @@ -1,27 +1,27 @@ -it('delay', { expose: { delay: 2500 } }, () => { - cy.visit('/options/delay'); +it("delay", { expose: { delay: 2500 } }, () => { + cy.visit("/options/delay"); }); -it('diff threshold', { expose: { diffThreshold: 1 } }, () => { - cy.visit('/options/diff-threshold'); +it("diff threshold", { expose: { diffThreshold: 1 } }, () => { + cy.visit("/options/diff-threshold"); }); -it('pause animation at end', { expose: { pauseAnimationAtEnd: true } }, () => { - cy.visit('/options/pause-animation-at-end'); +it("pause animation at end", { expose: { pauseAnimationAtEnd: true } }, () => { + cy.visit("/options/pause-animation-at-end"); }); -it('force high-contrast', { expose: { forcedColors: 'active' } }, () => { - cy.visit('/options/forced-colors'); +it("force high-contrast", { expose: { forcedColors: "active" } }, () => { + cy.visit("/options/forced-colors"); }); -it('prefers reduced motion', { expose: { prefersReducedMotion: 'reduce' } }, () => { - cy.visit('/options/prefers-reduced-motion'); +it("prefers reduced motion", { expose: { prefersReducedMotion: "reduce" } }, () => { + cy.visit("/options/prefers-reduced-motion"); }); -it('crops to viewport', { expose: { cropToViewport: true } }, () => { - cy.visit('/options/crop-to-viewport'); +it("crops to viewport", { expose: { cropToViewport: true } }, () => { + cy.visit("/options/crop-to-viewport"); }); -it('does not crop to viewport by default', () => { - cy.visit('/options/crop-to-viewport'); +it("does not crop to viewport by default", () => { + cy.visit("/options/crop-to-viewport"); }); diff --git a/packages/cypress/tests/cypress/e2e/snapshot-names.cy.ts b/packages/cypress/tests/cypress/e2e/snapshot-names.cy.ts index aae8b077..80ea176f 100644 --- a/packages/cypress/tests/cypress/e2e/snapshot-names.cy.ts +++ b/packages/cypress/tests/cypress/e2e/snapshot-names.cy.ts @@ -1,11 +1,11 @@ -it('in snapshot name', { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/'); +it("in snapshot name", { expose: { disableAutoSnapshot: true } }, () => { + cy.visit("/"); - cy.takeSnapshot('あ'); + cy.takeSnapshot("あ"); }); -it('in test case name あ', { expose: { disableAutoSnapshot: true } }, () => { - cy.visit('/'); +it("in test case name あ", { expose: { disableAutoSnapshot: true } }, () => { + cy.visit("/"); cy.takeSnapshot(); }); diff --git a/packages/cypress/tests/cypress/e2e/storybook.cy.ts b/packages/cypress/tests/cypress/e2e/storybook.cy.ts index 8b4d4145..5188d119 100644 --- a/packages/cypress/tests/cypress/e2e/storybook.cy.ts +++ b/packages/cypress/tests/cypress/e2e/storybook.cy.ts @@ -1,6 +1,6 @@ -it.skip('visits hosted storybook page', () => { +it.skip("visits hosted storybook page", () => { cy.visit( - 'https://main--653fef099b8957739e7534a4.chromatic.com/iframe.html?id=options-pause-animation-at-end--snapshot-1&viewMode=story' + "https://main--653fef099b8957739e7534a4.chromatic.com/iframe.html?id=options-pause-animation-at-end--snapshot-1&viewMode=story", ); - cy.contains('The image').should('be.visible'); + cy.contains("The image").should("be.visible"); }); diff --git a/packages/cypress/tests/cypress/e2e/viewports.cy.ts b/packages/cypress/tests/cypress/e2e/viewports.cy.ts index 7cdca27c..9655ce56 100644 --- a/packages/cypress/tests/cypress/e2e/viewports.cy.ts +++ b/packages/cypress/tests/cypress/e2e/viewports.cy.ts @@ -1,47 +1,47 @@ -describe('desktop', { viewportWidth: 1200, viewportHeight: 720 }, () => { - it('snapshots capture the correct viewport size', () => { - cy.visit('/viewports'); +describe("desktop", { viewportWidth: 1200, viewportHeight: 720 }, () => { + it("snapshots capture the correct viewport size", () => { + cy.visit("/viewports"); }); }); -context('mobile', { viewportWidth: 500, viewportHeight: 500 }, () => { - it('snapshots capture the correct viewport size', () => { - cy.visit('/viewports'); +context("mobile", { viewportWidth: 500, viewportHeight: 500 }, () => { + it("snapshots capture the correct viewport size", () => { + cy.visit("/viewports"); }); }); -context('using cy.viewport', () => { - it('does correctly set the viewport for Chromatic snapshots', () => { +context("using cy.viewport", () => { + it("does correctly set the viewport for Chromatic snapshots", () => { cy.viewport(800, 700); - cy.visit('/viewports'); + cy.visit("/viewports"); }); - it('snapshots capture multiple viewports inside a single test case', () => { - cy.visit('/viewports'); - cy.contains("I'm always rendered").should('be.visible'); - cy.takeSnapshot('default'); + it("snapshots capture multiple viewports inside a single test case", () => { + cy.visit("/viewports"); + cy.contains("I'm always rendered").should("be.visible"); + cy.takeSnapshot("default"); cy.viewport(480, 320); - cy.contains('Window width: 480').should('be.visible'); - cy.contains("I'm rendered when the page width is between 500-0").should('be.visible'); - cy.takeSnapshot('480 x 320'); + cy.contains("Window width: 480").should("be.visible"); + cy.contains("I'm rendered when the page width is between 500-0").should("be.visible"); + cy.takeSnapshot("480 x 320"); cy.viewport(850, 500); - cy.contains('Window width: 850').should('be.visible'); - cy.contains("I'm rendered when the page width is between 900-800").should('be.visible'); - cy.takeSnapshot('850 x 500'); + cy.contains("Window width: 850").should("be.visible"); + cy.contains("I'm rendered when the page width is between 900-800").should("be.visible"); + cy.takeSnapshot("850 x 500"); cy.viewport(1050, 1080); - cy.contains('Window width: 1050').should('be.visible'); - cy.contains("I'm rendered when the page width is between 1100-1000").should('be.visible'); - cy.takeSnapshot('1050 x 1080'); + cy.contains("Window width: 1050").should("be.visible"); + cy.contains("I'm rendered when the page width is between 1100-1000").should("be.visible"); + cy.takeSnapshot("1050 x 1080"); }); }); -context('using Cypress.config', () => { - it('does correctly set the viewport for Chromatic snapshots', () => { - Cypress.config('viewportWidth', 1150); - Cypress.config('viewportHeight', 500); - cy.visit('/viewports'); +context("using Cypress.config", () => { + it("does correctly set the viewport for Chromatic snapshots", () => { + Cypress.config("viewportWidth", 1150); + Cypress.config("viewportHeight", 500); + cy.visit("/viewports"); }); }); diff --git a/packages/cypress/tests/cypress/support/e2e.js b/packages/cypress/tests/cypress/support/e2e.js index 18300b37..79fda1f3 100644 --- a/packages/cypress/tests/cypress/support/e2e.js +++ b/packages/cypress/tests/cypress/support/e2e.js @@ -14,8 +14,8 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands'; -import '../../../dist/support.mjs'; +import "./commands"; +import "../../../dist/support.mjs"; // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/packages/cypress/tsdown.config.ts b/packages/cypress/tsdown.config.ts index 1ee767e3..1e2188cb 100644 --- a/packages/cypress/tsdown.config.ts +++ b/packages/cypress/tsdown.config.ts @@ -1,10 +1,10 @@ -import { defineConfig, type UserConfig } from 'tsdown'; +import { defineConfig, type UserConfig } from "tsdown"; const defaults: UserConfig = { - format: ['esm'], - platform: 'node', - target: 'esnext', - tsconfig: './tsconfig.json', + format: ["esm"], + platform: "node", + target: "esnext", + tsconfig: "./tsconfig.json", outputOptions: { comments: false }, minify: false, clean: true, @@ -14,25 +14,29 @@ const defaults: UserConfig = { export default defineConfig([ { ...defaults, - name: 'Node', + name: "Node", entry: { - index: 'src/index.ts', - support: 'src/support.ts', - 'bin/archive-storybook': 'src/bin/archive-storybook.ts', - 'bin/build-archive-storybook': 'src/bin/build-archive-storybook.ts', - 'storybook-config/main': 'src/storybook-config/main.ts', + index: "src/index.ts", + support: "src/support.ts", + "bin/archive-storybook": "src/bin/archive-storybook.ts", + "bin/build-archive-storybook": "src/bin/build-archive-storybook.ts", + "storybook-config/main": "src/storybook-config/main.ts", }, - dts: { entry: ['src/index.ts', 'src/support.ts'] }, - deps: { onlyBundle: ['@rrweb/types', 'mime', 'srcset'] }, + dts: { + entry: ["src/index.ts", "src/support.ts"], + // tsgolint rejects `baseUrl` in tsconfig files, but tsdown's dts type inference needs it: + compilerOptions: { baseUrl: "../.." }, + }, + deps: { onlyBundle: ["@rrweb/types", "mime", "srcset"] }, }, { ...defaults, - name: 'Browser', + name: "Browser", entry: { - 'storybook-config/manager': 'src/storybook-config/shared/manager.ts', - 'storybook-config/preview': 'src/storybook-config/shared/preview.ts', + "storybook-config/manager": "src/storybook-config/shared/manager.ts", + "storybook-config/preview": "src/storybook-config/shared/preview.ts", }, - platform: 'browser', - deps: { onlyBundle: ['@rrweb/types'] }, + platform: "browser", + deps: { onlyBundle: ["@rrweb/types"] }, }, ]); diff --git a/packages/cypress/vitest.config.ts b/packages/cypress/vitest.config.ts index 301bd6ed..191f93dd 100644 --- a/packages/cypress/vitest.config.ts +++ b/packages/cypress/vitest.config.ts @@ -1,13 +1,13 @@ -import { defineProject } from 'vitest/config'; +import { defineProject } from "vitest/config"; export default defineProject({ resolve: { tsconfigPaths: true }, test: { - name: { label: 'Cypress', color: 'green' }, - include: ['src/**/*.test.ts', 'embedded.test.ts'], + name: { label: "Cypress", color: "green" }, + include: ["src/**/*.test.ts", "embedded.test.ts"], typecheck: { enabled: true, - include: ['src/*.test-d.ts'], + include: ["src/*.test-d.ts"], ignoreSourceErrors: true, }, }, diff --git a/packages/playwright/embedded.test.ts b/packages/playwright/embedded.test.ts index e30e2f6b..8092d308 100644 --- a/packages/playwright/embedded.test.ts +++ b/packages/playwright/embedded.test.ts @@ -1,19 +1,19 @@ -import { expect, it } from 'vitest'; import { collectEmbeddedFiles, generateEmbeddedChecksum, -} from '@chromatic-com/shared-e2e/test-utils'; +} from "@chromatic-com/shared-e2e/test-utils"; +import { expect, it } from "vitest"; -it('/embedded/node_modules has not changed', () => { +it("/embedded/node_modules has not changed", () => { const { hash, fileCount } = generateEmbeddedChecksum(import.meta.dirname); expect(fileCount).toMatchInlineSnapshot(`8198`); expect(hash).toMatchInlineSnapshot( - `"fb7a3c92fbe7930617d966322c4b20b3918a3e197422c2850b6f8838a9d8ad8c"` + `"fb7a3c92fbe7930617d966322c4b20b3918a3e197422c2850b6f8838a9d8ad8c"`, ); }); -it('/embedded/node_modules tree snapshot', () => { +it("/embedded/node_modules tree snapshot", () => { expect(collectEmbeddedFiles(import.meta.dirname)).toMatchInlineSnapshot(` [ "@babel/code-frame@7.29.7", diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 2db9cd4c..fabf249a 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -2,17 +2,25 @@ "name": "@chromatic-com/playwright", "version": "0.14.12", "description": "Chromatic Visual Regression Testing for Playwright", + "license": "MIT", + "author": "Chromatic ", "repository": { "type": "git", "url": "git+https://github.com/chromaui/chromatic-e2e.git", "directory": "packages/playwright" }, - "license": "MIT", - "author": "Chromatic ", - "type": "module", - "engines": { - "node": ">=22" + "bin": { + "archive-storybook": "dist/bin/archive-storybook.mjs", + "build-archive-storybook": "dist/bin/build-archive-storybook.mjs" }, + "files": [ + "dist", + "embedded", + "README.md" + ], + "type": "module", + "main": "dist/index.mjs", + "types": "dist/index.d.mts", "exports": { ".": { "types": "./dist/index.d.mts", @@ -23,17 +31,9 @@ "./bin/build-archive-storybook": "./dist/bin/build-archive-storybook.mjs", "./package.json": "./package.json" }, - "main": "dist/index.mjs", - "types": "dist/index.d.mts", - "bin": { - "archive-storybook": "dist/bin/archive-storybook.mjs", - "build-archive-storybook": "dist/bin/build-archive-storybook.mjs" + "publishConfig": { + "access": "public" }, - "files": [ - "dist", - "embedded", - "README.md" - ], "scripts": { "build": "tsdown", "test:playwright": "playwright test", @@ -59,7 +59,7 @@ "peerDependencies": { "@playwright/test": "^1.0.0" }, - "publishConfig": { - "access": "public" + "engines": { + "node": ">=22" } } diff --git a/packages/playwright/playwright.config.ts b/packages/playwright/playwright.config.ts index 63b1639e..9f250420 100644 --- a/packages/playwright/playwright.config.ts +++ b/packages/playwright/playwright.config.ts @@ -1,21 +1,21 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ - testDir: './tests', + testDir: "./tests", use: { - baseURL: 'http://localhost:3000', + baseURL: "http://localhost:3000", httpCredentials: { - username: 'user', - password: 'secret', + username: "user", + password: "secret", }, - ...devices['Desktop Chrome'], + ...devices["Desktop Chrome"], viewport: { width: 1280, height: 720 }, }, webServer: { - command: 'pnpm --filter @chromaui/chromatic-e2e dev:server', - url: 'http://127.0.0.1:3000', + command: "pnpm --filter @chromaui/chromatic-e2e dev:server", + url: "http://127.0.0.1:3000", reuseExistingServer: !process.env.CI, }, }); diff --git a/packages/playwright/src/bin/archive-storybook.ts b/packages/playwright/src/bin/archive-storybook.ts index 558decba..434dcb0a 100644 --- a/packages/playwright/src/bin/archive-storybook.ts +++ b/packages/playwright/src/bin/archive-storybook.ts @@ -1,12 +1,14 @@ #!/usr/bin/env node -import { archiveStorybook } from '@chromatic-com/shared-e2e/archive-storybook'; -import path from 'path'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; +import path from "node:path"; + +import { archiveStorybook } from "@chromatic-com/shared-e2e/archive-storybook"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; // Discard first two entries (exec path and file path) const args = process.argv.slice(2); -const configDir = path.resolve(import.meta.dirname, '../storybook-config'); +const configDir = path.resolve(import.meta.dirname, "../storybook-config"); try { await archiveStorybook(args, configDir, DEFAULT_OUTPUT_DIR); diff --git a/packages/playwright/src/bin/build-archive-storybook.ts b/packages/playwright/src/bin/build-archive-storybook.ts index 38e4dc88..e3a50a4a 100644 --- a/packages/playwright/src/bin/build-archive-storybook.ts +++ b/packages/playwright/src/bin/build-archive-storybook.ts @@ -1,12 +1,14 @@ #!/usr/bin/env node -import { buildArchiveStorybook } from '@chromatic-com/shared-e2e/archive-storybook'; -import path from 'path'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; +import path from "node:path"; + +import { buildArchiveStorybook } from "@chromatic-com/shared-e2e/archive-storybook"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; // Discard first two entries (exec path and file path) const args = process.argv.slice(2); -const configDir = path.resolve(import.meta.dirname, '../storybook-config'); +const configDir = path.resolve(import.meta.dirname, "../storybook-config"); try { await buildArchiveStorybook(args, configDir, DEFAULT_OUTPUT_DIR); diff --git a/packages/playwright/src/browser.ts b/packages/playwright/src/browser.ts index 83cf2c2c..f5ba9ad3 100644 --- a/packages/playwright/src/browser.ts +++ b/packages/playwright/src/browser.ts @@ -1,6 +1,6 @@ -import { snapshot, createMirror } from '@chromaui/rrweb-snapshot'; -import { type DOMSnapshots } from '@chromatic-com/shared-e2e'; -import { type serializedNodeWithId } from '@rrweb/types'; +import { type DOMSnapshots } from "@chromatic-com/shared-e2e"; +import { snapshot, createMirror } from "@chromaui/rrweb-snapshot"; +import { type serializedNodeWithId } from "@rrweb/types"; export type { takeSnapshot }; @@ -8,11 +8,11 @@ async function takeSnapshot() { const mirror = createMirror(); const domSnapshot = snapshot(document, { recordCanvas: true, mirror }); - const colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const colorScheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - const pseudoClassIds: DOMSnapshots[string]['pseudoClassIds'] = {}; + const pseudoClassIds: DOMSnapshots[string]["pseudoClassIds"] = {}; - for (const className of [':hover', ':focus', ':focus-visible', ':active'] as const) { + for (const className of [":hover", ":focus", ":focus-visible", ":active"] as const) { const elements = document.querySelectorAll(className); const ids = Array.from(elements, (el) => mirror.getId(el)).filter((id) => id !== -1); pseudoClassIds[className] = ids; @@ -24,26 +24,26 @@ async function takeSnapshot() { } async function replaceBlobUrls(node: serializedNodeWithId) { - if (!('childNodes' in node)) { + if (!("childNodes" in node)) { return; } await Promise.all( node.childNodes.map(async (childNode) => { if ( - 'tagName' in childNode && - childNode.tagName === 'img' && - typeof childNode.attributes.src === 'string' && - childNode.attributes.src?.startsWith('blob:') + "tagName" in childNode && + childNode.tagName === "img" && + typeof childNode.attributes.src === "string" && + childNode.attributes.src?.startsWith("blob:") ) { const base64Url = await toDataURL(childNode.attributes.src); childNode.attributes.src = base64Url; } - if ('childNodes' in childNode && childNode.childNodes?.length) { + if ("childNodes" in childNode && childNode.childNodes?.length) { await replaceBlobUrls(childNode); } - }) + }), ); } @@ -52,7 +52,8 @@ async function toDataURL(url: string): Promise { return new Promise((resolveFileRead, reject) => { const reader = new FileReader(); - reader.onloadend = () => resolveFileRead(reader.result?.toString() || ''); + // oxlint-disable-next-line typescript/no-base-to-string + reader.onloadend = () => resolveFileRead(reader.result?.toString() || ""); reader.onerror = reject; // convert the blob to base64 string diff --git a/packages/playwright/src/constants.ts b/packages/playwright/src/constants.ts index 3695930f..3b835cb3 100644 --- a/packages/playwright/src/constants.ts +++ b/packages/playwright/src/constants.ts @@ -1 +1 @@ -export const DEFAULT_OUTPUT_DIR = 'test-results'; +export const DEFAULT_OUTPUT_DIR = "test-results"; diff --git a/packages/playwright/src/createResourceArchive.test.ts b/packages/playwright/src/createResourceArchive.test.ts index 7c5a76ee..a6ac593c 100644 --- a/packages/playwright/src/createResourceArchive.test.ts +++ b/packages/playwright/src/createResourceArchive.test.ts @@ -1,11 +1,12 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { dedent } from 'ts-dedent'; -import express, { type Request } from 'express'; -import { Server } from 'http'; -import { Browser, chromium, Page } from 'playwright'; +import { Server } from "node:http"; -import { logger } from '@chromatic-com/shared-e2e'; -import { createResourceArchive } from './createResourceArchive'; +import { logger } from "@chromatic-com/shared-e2e"; +import express, { type Request } from "express"; +import { Browser, chromium, Page } from "playwright"; +import { dedent } from "ts-dedent"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { createResourceArchive } from "./createResourceArchive"; const TEST_PORT = 13337; @@ -29,25 +30,25 @@ const styleCss = dedent` `; const imgPng = - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; // stubbed external imgs so we're not relying on a placeholder -const externalImgPng = 'iamexternal'; -const anotherExternalImg = 'anotherone'; +const externalImgPng = "iamexternal"; +const anotherExternalImg = "anotherone"; const pathToResponseInfo = { - '/': { - content: ({ query: { inject = '' } }: Request) => - indexHtml.replace('', `${[].concat(inject).map(decodeURIComponent).join('')}`), - mimeType: 'text/html', + "/": { + content: ({ query: { inject = "" } }: Request) => + indexHtml.replace("", `${[].concat(inject).map(decodeURIComponent).join("")}`), + mimeType: "text/html", }, - '/style.css': { + "/style.css": { content: styleCss, - mimeType: 'text/css', + mimeType: "text/css", }, - '/img.png': { - content: Buffer.from(imgPng, 'base64'), - mimeType: 'image/png', + "/img.png": { + content: Buffer.from(imgPng, "base64"), + mimeType: "image/png", }, } as const; @@ -59,8 +60,8 @@ beforeEach(async () => { Object.entries(pathToResponseInfo).forEach(([path, responseInfo]) => { app.get(path, (req, res) => { const { content, mimeType } = responseInfo; - res.header('content-type', mimeType); - res.send(typeof content === 'function' ? content(req) : content); + res.header("content-type", mimeType); + res.send(typeof content === "function" ? content(req) : content); }); }); @@ -69,14 +70,14 @@ beforeEach(async () => { }); }); -afterEach(async () => { - await server.close(); +afterEach(() => { + server.close(); }); -describe('new', () => { +describe("new", () => { let browser: Browser; let page: Page; - const mockWarn = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const mockWarn = vi.spyOn(logger, "warn").mockImplementation(() => {}); beforeEach(async () => { // create a bare-bones Playwright test launch (https://playwright.dev/docs/library) @@ -84,16 +85,16 @@ describe('new', () => { page = await browser.newPage(); // mock external image requests - await page.route('https://i-ama.fake/external/domain/image.png', async (route) => { - await route.fulfill({ body: Buffer.from(externalImgPng, 'base64') }); + await page.route("https://i-ama.fake/external/domain/image.png", async (route) => { + await route.fulfill({ body: Buffer.from(externalImgPng, "base64") }); }); - await page.route('https://another-domain.com/picture.png', async (route) => { - await route.fulfill({ body: Buffer.from(anotherExternalImg, 'base64') }); + await page.route("https://another-domain.com/picture.png", async (route) => { + await route.fulfill({ body: Buffer.from(anotherExternalImg, "base64") }); }); - await page.route('https://unwanted-domain.com/img.png', async (route) => { - await route.fulfill({ body: Buffer.from(anotherExternalImg, 'base64') }); + await page.route("https://unwanted-domain.com/img.png", async (route) => { + await route.fulfill({ body: Buffer.from(anotherExternalImg, "base64") }); }); }); @@ -101,7 +102,7 @@ describe('new', () => { await browser.close(); }); - it('should log if the network times out waiting for requests', async () => { + it("should log if the network times out waiting for requests", async () => { const complete = await createResourceArchive({ page, networkTimeout: 1 }); await page.goto(baseUrl); @@ -111,7 +112,7 @@ describe('new', () => { expect(mockWarn).toBeCalledWith(`Global timeout of 1ms reached`); }); - it('gathers basic resources used by the page', async () => { + it("gathers basic resources used by the page", async () => { const complete = await createResourceArchive({ page }); await page.goto(baseUrl); @@ -119,23 +120,23 @@ describe('new', () => { const archive = await complete(); expect(archive).toEqual({ - 'http://localhost:13337/style.css': { + "http://localhost:13337/style.css": { statusCode: 200, - statusText: 'OK', + statusText: "OK", body: Buffer.from(styleCss), - contentType: 'text/css; charset=utf-8', + contentType: "text/css; charset=utf-8", }, - 'http://localhost:13337/img.png': { + "http://localhost:13337/img.png": { statusCode: 200, - statusText: 'OK', - body: Buffer.from(imgPng, 'base64'), - contentType: 'image/png', + statusText: "OK", + body: Buffer.from(imgPng, "base64"), + contentType: "image/png", }, }); }); - it('ignores remote resources', async () => { - const externalUrl = 'https://i-ama.fake/external/domain/image.png'; + it("ignores remote resources", async () => { + const externalUrl = "https://i-ama.fake/external/domain/image.png"; const indexPath = `/?inject=${encodeURIComponent(``)}`; const complete = await createResourceArchive({ page }); @@ -145,38 +146,38 @@ describe('new', () => { const archive = await complete(); expect(archive).toEqual({ - 'http://localhost:13337/style.css': { + "http://localhost:13337/style.css": { statusCode: 200, - statusText: 'OK', + statusText: "OK", body: Buffer.from(styleCss), - contentType: 'text/css; charset=utf-8', + contentType: "text/css; charset=utf-8", }, - 'http://localhost:13337/img.png': { + "http://localhost:13337/img.png": { statusCode: 200, - statusText: 'OK', - body: Buffer.from(imgPng, 'base64'), - contentType: 'image/png', + statusText: "OK", + body: Buffer.from(imgPng, "base64"), + contentType: "image/png", }, }); }); - it('includes remote resource when told to', async () => { + it("includes remote resource when told to", async () => { const externalUrls = [ - 'https://i-ama.fake/external/domain/image.png', - 'https://another-domain.com/picture.png', + "https://i-ama.fake/external/domain/image.png", + "https://another-domain.com/picture.png", // this image won't be in allow-list - 'https://unwanted-domain.com/img.png', + "https://unwanted-domain.com/img.png", ]; const indexPath = `/?inject=${encodeURIComponent( - externalUrls.map((url) => ``).join() + externalUrls.map((url) => ``).join(), )}`; const complete = await createResourceArchive({ page, assetDomains: [ // external domains we allow-list - 'i-ama.fake', - 'another-domain.com', + "i-ama.fake", + "another-domain.com", ], }); @@ -185,29 +186,29 @@ describe('new', () => { const archive = await complete(); expect(archive).toEqual({ - 'http://localhost:13337/style.css': { + "http://localhost:13337/style.css": { statusCode: 200, - statusText: 'OK', + statusText: "OK", body: Buffer.from(styleCss), - contentType: 'text/css; charset=utf-8', + contentType: "text/css; charset=utf-8", }, - 'http://localhost:13337/img.png': { + "http://localhost:13337/img.png": { statusCode: 200, - statusText: 'OK', - body: Buffer.from(imgPng, 'base64'), - contentType: 'image/png', + statusText: "OK", + body: Buffer.from(imgPng, "base64"), + contentType: "image/png", }, // includes cross-origin images - 'https://i-ama.fake/external/domain/image.png': { + "https://i-ama.fake/external/domain/image.png": { statusCode: 200, - statusText: 'OK', - body: Buffer.from(externalImgPng, 'base64'), + statusText: "OK", + body: Buffer.from(externalImgPng, "base64"), contentType: undefined, }, - 'https://another-domain.com/picture.png': { + "https://another-domain.com/picture.png": { statusCode: 200, - statusText: 'OK', - body: Buffer.from(anotherExternalImg, 'base64'), + statusText: "OK", + body: Buffer.from(anotherExternalImg, "base64"), contentType: undefined, }, }); diff --git a/packages/playwright/src/createResourceArchive.ts b/packages/playwright/src/createResourceArchive.ts index ec9f1968..9af9a6cf 100644 --- a/packages/playwright/src/createResourceArchive.ts +++ b/packages/playwright/src/createResourceArchive.ts @@ -1,11 +1,11 @@ -import type { Page } from '@playwright/test'; import { ResourceArchiver, ResourceArchive, DEFAULT_GLOBAL_RESOURCE_ARCHIVE_TIMEOUT_MS, logger, HttpCredentials, -} from '@chromatic-com/shared-e2e'; +} from "@chromatic-com/shared-e2e"; +import type { Page } from "@playwright/test"; const idle = async (page: Page, networkTimeoutMs = DEFAULT_GLOBAL_RESOURCE_ARCHIVE_TIMEOUT_MS) => { let globalNetworkTimerId: null | ReturnType = null; @@ -26,7 +26,7 @@ const idle = async (page: Page, networkTimeoutMs = DEFAULT_GLOBAL_RESOURCE_ARCHI // The second promise wraps a network idle timeout. This uses playwright's built-in functionality to detect when the network // is idle. - const networkIdlePromise = page.waitForLoadState('networkidle').finally(() => { + const networkIdlePromise = page.waitForLoadState("networkidle").finally(() => { clearTimeout(globalNetworkTimerId); }); diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 41683871..cccc5f03 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -1,9 +1,9 @@ -import { test as base, expect } from '@playwright/test'; +import { test as base, expect } from "@playwright/test"; -import { makeTest } from './makeTest'; +import { makeTest } from "./makeTest"; export const test = makeTest(base); export { expect }; -export { takeSnapshot } from './takeSnapshot'; -export type { ChromaticConfig } from '@chromatic-com/shared-e2e'; +export { takeSnapshot } from "./takeSnapshot"; +export type { ChromaticConfig } from "@chromatic-com/shared-e2e"; diff --git a/packages/playwright/src/makeTest.test.ts b/packages/playwright/src/makeTest.test.ts index a01a7e7b..2e184498 100644 --- a/packages/playwright/src/makeTest.test.ts +++ b/packages/playwright/src/makeTest.test.ts @@ -1,10 +1,11 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { TestInfo } from 'playwright/test'; -import { Browser, chromium, Page } from 'playwright'; -import { performChromaticSnapshot } from './makeTest'; -import { chromaticSnapshots, takeSnapshot } from './takeSnapshot'; +import { Browser, chromium, Page } from "playwright"; +import { TestInfo } from "playwright/test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -describe('makeTest', () => { +import { performChromaticSnapshot } from "./makeTest"; +import { chromaticSnapshots, takeSnapshot } from "./takeSnapshot"; + +describe("makeTest", () => { let browser: Browser; let page: Page; let pageTwo: Page; @@ -24,12 +25,12 @@ describe('makeTest', () => { }); const mockTestInfo: Partial = { - titlePath: ['example'], - outputDir: '', - testId: 'a', + titlePath: ["example"], + outputDir: "", + testId: "a", }; - it('Removes the entry from the snapshots after Playwright test is done', async () => { + it("Removes the entry from the snapshots after Playwright test is done", async () => { expect(chromaticSnapshots.size).toBe(0); await performChromaticSnapshot( { page }, @@ -38,12 +39,12 @@ describe('makeTest', () => { }, // @ts-expect-error mockTestInfo doesn't try to have all the TestInfo fields. I didn't see a way of deriving testInfo // from the page/browser itself (typically that comes as part of the Playwright test runner, which we're outside of here) - mockTestInfo + mockTestInfo, ); expect(chromaticSnapshots.size).toBe(0); }); - it('Removes entry even if Playwright test throws an error', async () => { + it("Removes entry even if Playwright test throws an error", async () => { expect(chromaticSnapshots.size).toBe(0); // make sure we still throw the original error (we don't want to swallow errors from the user's test) await expect( @@ -54,17 +55,17 @@ describe('makeTest', () => { // pretend to take a snapshot before we throw the error // (so chromaticSnapshots has something on it for this test that we expect will be removed) await takeSnapshot(page, mockTestInfo); - throw new Error('Something is wrong'); + throw new Error("Something is wrong"); }, // @ts-expect-error mockTestInfo doesn't try to have all TestInfo fields - mockTestInfo - ) - ).rejects.toThrow('Something is wrong'); + mockTestInfo, + ), + ).rejects.toThrow("Something is wrong"); // make sure we still remove the entry even if performChromaticSnapshot encounters an error expect(chromaticSnapshots.size).toBe(0); }); - it('Removes all entries from the snapshots when there are multiple Playwright tests', async () => { + it("Removes all entries from the snapshots when there are multiple Playwright tests", async () => { expect(chromaticSnapshots.size).toBe(0); await performChromaticSnapshot( { page }, @@ -72,7 +73,7 @@ describe('makeTest', () => { // do-nothing use() function }, // @ts-expect-error mockTestInfo doesn't try to have all the TestInfo fields - mockTestInfo + mockTestInfo, ); // simulates another test @@ -82,7 +83,7 @@ describe('makeTest', () => { // do-nothing use() function }, // @ts-expect-error mockTestInfo doesn't try to have all the TestInfo fields - { ...mockTestInfo, testId: 'b' } + { ...mockTestInfo, testId: "b" }, ); expect(chromaticSnapshots.size).toBe(0); }); diff --git a/packages/playwright/src/makeTest.ts b/packages/playwright/src/makeTest.ts index aecf6c11..5829718c 100644 --- a/packages/playwright/src/makeTest.ts +++ b/packages/playwright/src/makeTest.ts @@ -1,3 +1,10 @@ +import { join } from "node:path"; + +import type { ChromaticConfig } from "@chromatic-com/shared-e2e"; +import { + writeTestResult, + DEFAULT_GLOBAL_RESOURCE_ARCHIVE_TIMEOUT_MS, +} from "@chromatic-com/shared-e2e"; import type { TestType, PlaywrightTestArgs, @@ -6,15 +13,10 @@ import type { PlaywrightWorkerOptions, TestInfo, Page, -} from '@playwright/test'; -import type { ChromaticConfig } from '@chromatic-com/shared-e2e'; -import { - writeTestResult, - DEFAULT_GLOBAL_RESOURCE_ARCHIVE_TIMEOUT_MS, -} from '@chromatic-com/shared-e2e'; -import { join } from 'node:path'; -import { chromaticSnapshots, takeSnapshot } from './takeSnapshot'; -import { createResourceArchive } from './createResourceArchive'; +} from "@playwright/test"; + +import { createResourceArchive } from "./createResourceArchive"; +import { chromaticSnapshots, takeSnapshot } from "./takeSnapshot"; export const performChromaticSnapshot = async ( { @@ -32,7 +34,7 @@ export const performChromaticSnapshot = async ( ignoreSelectors, }: ChromaticConfig & { page: Page }, use: () => Promise, - testInfo: TestInfo + testInfo: TestInfo, ) => { const { testId, project } = testInfo; const httpCredentials = project?.use?.httpCredentials; @@ -42,7 +44,7 @@ export const performChromaticSnapshot = async ( // We can later snapshot them in different browsers in the cloud. // TODO: I'm not sure if this is the best way to detect the browser version, but // it seems to work - if (page.context().browser().browserType().name() !== 'chromium') { + if (page.context().browser().browserType().name() !== "chromium") { await use(); return; } @@ -65,7 +67,7 @@ export const performChromaticSnapshot = async ( // TestInfo.outputDir gives us the test-specific subfolder (https://playwright.dev/docs/api/class-testconfig#test-config-output-dir); // we want to write one level above that - const outputDir = join(testInfo.outputDir, '..'); + const outputDir = join(testInfo.outputDir, ".."); await writeTestResult( { ...testInfo, outputDir, pageUrl: page.url() }, Object.fromEntries(snapshots), @@ -80,7 +82,7 @@ export const performChromaticSnapshot = async ( cropToViewport, ignoreSelectors, }, - 'playwright' + "playwright", ); } finally { // make sure we clear the value associated with this test ID, so the shared chromaticSnapshots object stays small @@ -97,7 +99,7 @@ export const makeTest = ( base: TestType< PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions - > + >, ) => base.extend({ // ChromaticConfig defaults diff --git a/packages/playwright/src/storybook-config/main.ts b/packages/playwright/src/storybook-config/main.ts index a8d13d02..750a4b3e 100644 --- a/packages/playwright/src/storybook-config/main.ts +++ b/packages/playwright/src/storybook-config/main.ts @@ -1,30 +1,32 @@ -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; -import type { StorybookConfig } from '@storybook/server-webpack5'; -import { archivesDir } from '@chromatic-com/shared-e2e/utils/filePaths'; -import { storybookParentNodeModulesDir } from '@chromatic-com/shared-e2e/utils/storybookPaths'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { archivesDir } from "@chromatic-com/shared-e2e/utils/filePaths"; +import { storybookParentNodeModulesDir } from "@chromatic-com/shared-e2e/utils/storybookPaths"; +import type { StorybookConfig } from "@storybook/server-webpack5"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; // Resolve embedded Storybook 10.x stack from package root (dist/storybook-config -> .. -> ..) -const packageRoot = path.resolve(import.meta.dirname, '..', '..'); -const embeddedDir = path.join(packageRoot, 'embedded', 'node_modules'); +const packageRoot = path.resolve(import.meta.dirname, "..", ".."); +const embeddedDir = path.join(packageRoot, "embedded", "node_modules"); export default { - stories: [path.resolve(archivesDir(DEFAULT_OUTPUT_DIR), '*.stories.json')], - managerEntries: [path.resolve(import.meta.dirname, 'manager.mjs')], - previewAnnotations: [path.resolve(import.meta.dirname, 'preview.mjs')], + stories: [path.resolve(archivesDir(DEFAULT_OUTPUT_DIR), "*.stories.json")], + managerEntries: [path.resolve(import.meta.dirname, "manager.mjs")], + previewAnnotations: [path.resolve(import.meta.dirname, "preview.mjs")], framework: { - name: path.join(embeddedDir, '@storybook', 'server-webpack5'), + name: path.join(embeddedDir, "@storybook", "server-webpack5"), options: {}, }, core: { // ESM does not support directory imports; point to the package entry file. builder: pathToFileURL( - path.join(embeddedDir, '@storybook', 'builder-webpack5', 'dist', 'index.js') + path.join(embeddedDir, "@storybook", "builder-webpack5", "dist", "index.js"), ).href, - renderer: pathToFileURL(path.join(embeddedDir, '@storybook', 'server', 'preset.js')).href, + renderer: pathToFileURL(path.join(embeddedDir, "@storybook", "server", "preset.js")).href, }, - staticDirs: [path.resolve(archivesDir(DEFAULT_OUTPUT_DIR), 'archive')], + staticDirs: [path.resolve(archivesDir(DEFAULT_OUTPUT_DIR), "archive")], features: { sidebarOnboardingChecklist: false, }, @@ -36,7 +38,7 @@ export default { if (storybookNodeModules) { config.resolve ??= {}; config.resolve.modules = [ - ...(config.resolve.modules ?? ['node_modules']), + ...(config.resolve.modules ?? ["node_modules"]), storybookNodeModules, ]; } diff --git a/packages/playwright/src/takeSnapshot.test.ts b/packages/playwright/src/takeSnapshot.test.ts index a1dab6be..67952048 100644 --- a/packages/playwright/src/takeSnapshot.test.ts +++ b/packages/playwright/src/takeSnapshot.test.ts @@ -1,8 +1,9 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { TestInfo } from 'playwright/test'; -import { Page } from 'playwright'; -import { NodeType, serializedElementNodeWithId } from '@rrweb/types'; -import { chromaticSnapshots, takeSnapshot } from './takeSnapshot'; +import { NodeType, serializedElementNodeWithId } from "@rrweb/types"; +import { Page } from "playwright"; +import { TestInfo } from "playwright/test"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { chromaticSnapshots, takeSnapshot } from "./takeSnapshot"; // @ts-expect-error fakePage isn't a proper page, but we just need to fake enough to run the test // (where page is probably too tied up into takeSnapshot anyway) @@ -13,65 +14,65 @@ const fakePage = { frames: () => [], } as Page; -describe('Snapshot storage', () => { +describe("Snapshot storage", () => { beforeEach(async () => { // we have to manually clear out the chromaticSnapshots entries since that behavior only happens // in our test fixture (one level up). chromaticSnapshots.clear(); }); - it('creates an entry (test name and snapshot buffer) when a snapshot is taken', async () => { + it("creates an entry (test name and snapshot buffer) when a snapshot is taken", async () => { expect(chromaticSnapshots.size).toBe(0); // not ideal to mock testInfo, but AFAIK we can't get testInfo when using Playwright library instead of Playwright test runner. // and this way we can specify the test ID ourselves - const fakeTestInfo = { testId: 'a' }; + const fakeTestInfo = { testId: "a" }; await takeSnapshot(fakePage, fakeTestInfo as TestInfo); - expect(chromaticSnapshots.get('a').has('Snapshot #1')).toBe(true); - expect(Buffer.isBuffer(chromaticSnapshots.get('a').get('Snapshot #1')?.snapshot)).toBe(true); - expect(chromaticSnapshots.get('a').get('Snapshot #1')?.viewport).toEqual({ + expect(chromaticSnapshots.get("a").has("Snapshot #1")).toBe(true); + expect(Buffer.isBuffer(chromaticSnapshots.get("a").get("Snapshot #1")?.snapshot)).toBe(true); + expect(chromaticSnapshots.get("a").get("Snapshot #1")?.viewport).toEqual({ width: 100, height: 200, }); }); - it('creates multiple entries when multiple snapshots are taken', async () => { + it("creates multiple entries when multiple snapshots are taken", async () => { expect(chromaticSnapshots.size).toBe(0); - const fakeTestInfo = { testId: 'a' }; + const fakeTestInfo = { testId: "a" }; // take multiple snapshots await takeSnapshot(fakePage, fakeTestInfo as TestInfo); await takeSnapshot(fakePage, fakeTestInfo as TestInfo); - expect(chromaticSnapshots.get('a').has('Snapshot #1')).toBe(true); - expect(chromaticSnapshots.get('a').has('Snapshot #2')).toBe(true); + expect(chromaticSnapshots.get("a").has("Snapshot #1")).toBe(true); + expect(chromaticSnapshots.get("a").has("Snapshot #2")).toBe(true); }); - it('preserves names of snapshots when provided', async () => { + it("preserves names of snapshots when provided", async () => { expect(chromaticSnapshots.size).toBe(0); - const fakeTestInfo = { testId: 'a' }; - await takeSnapshot(fakePage, 'first snappy', fakeTestInfo as TestInfo); - await takeSnapshot(fakePage, 'second snappy', fakeTestInfo as TestInfo); + const fakeTestInfo = { testId: "a" }; + await takeSnapshot(fakePage, "first snappy", fakeTestInfo as TestInfo); + await takeSnapshot(fakePage, "second snappy", fakeTestInfo as TestInfo); - expect(chromaticSnapshots.get('a').has('first snappy')).toBe(true); - expect(chromaticSnapshots.get('a').has('second snappy')).toBe(true); + expect(chromaticSnapshots.get("a").has("first snappy")).toBe(true); + expect(chromaticSnapshots.get("a").has("second snappy")).toBe(true); }); - it('captures iframe contents', async () => { + it("captures iframe contents", async () => { const iframes: serializedElementNodeWithId[] = [ { type: NodeType.Element, - tagName: 'iframe', - attributes: { src: 'https://example1.com' }, + tagName: "iframe", + attributes: { src: "https://example1.com" }, childNodes: [], id: 2, }, { type: NodeType.Element, - tagName: 'iframe', - attributes: { src: 'https://example2.com' }, + tagName: "iframe", + attributes: { src: "https://example2.com" }, childNodes: [], id: 3, }, @@ -83,7 +84,7 @@ describe('Snapshot storage', () => { JSON.stringify({ domSnapshot: { type: NodeType.Document, - childNodes: [{ type: NodeType.Element, tagName: 'html', childNodes: iframes, id: 1 }], + childNodes: [{ type: NodeType.Element, tagName: "html", childNodes: iframes, id: 1 }], id: 0, }, pseudoClassIds: {}, @@ -94,40 +95,40 @@ describe('Snapshot storage', () => { { evaluate: () => JSON.stringify({ - domSnapshot: { type: NodeType.Element, tagName: 'div', textContent: 'One', id: 10 }, - pseudoClassIds: { ':hover': [10] }, + domSnapshot: { type: NodeType.Element, tagName: "div", textContent: "One", id: 10 }, + pseudoClassIds: { ":hover": [10] }, }), url: () => iframes[0].attributes.src, }, { evaluate: () => JSON.stringify({ - domSnapshot: { type: NodeType.Element, tagName: 'span', textContent: 'Two', id: 20 }, - pseudoClassIds: { ':focus': [20] }, + domSnapshot: { type: NodeType.Element, tagName: "span", textContent: "Two", id: 20 }, + pseudoClassIds: { ":focus": [20] }, }), url: () => iframes[1].attributes.src, }, ], } as unknown as Page; - const fakeTestInfo = { testId: 'multi-iframe-test' }; + const fakeTestInfo = { testId: "multi-iframe-test" }; await takeSnapshot(pageWithIframes, fakeTestInfo as TestInfo); - expect(chromaticSnapshots.get('multi-iframe-test').has('Snapshot #1')).toBe(true); - const snapshot = chromaticSnapshots.get('multi-iframe-test').get('Snapshot #1')?.snapshot; + expect(chromaticSnapshots.get("multi-iframe-test").has("Snapshot #1")).toBe(true); + const snapshot = chromaticSnapshots.get("multi-iframe-test").get("Snapshot #1")?.snapshot; const parsedSnapshot = JSON.parse(snapshot!.toString()); const capturedIframes = parsedSnapshot.childNodes[0].childNodes; expect(capturedIframes.length).toBe(2); - expect(capturedIframes[0].tagName).toBe('iframe'); - expect(capturedIframes[0].contentDocument?.tagName).toBe('div'); - expect(capturedIframes[0].contentDocument?.textContent).toBe('One'); - expect(capturedIframes[0].pseudoClassIds).toEqual({ ':hover': [10] }); + expect(capturedIframes[0].tagName).toBe("iframe"); + expect(capturedIframes[0].contentDocument?.tagName).toBe("div"); + expect(capturedIframes[0].contentDocument?.textContent).toBe("One"); + expect(capturedIframes[0].pseudoClassIds).toEqual({ ":hover": [10] }); - expect(capturedIframes[1].tagName).toBe('iframe'); - expect(capturedIframes[1].contentDocument?.tagName).toBe('span'); - expect(capturedIframes[1].contentDocument?.textContent).toBe('Two'); - expect(capturedIframes[1].pseudoClassIds).toEqual({ ':focus': [20] }); + expect(capturedIframes[1].tagName).toBe("iframe"); + expect(capturedIframes[1].contentDocument?.tagName).toBe("span"); + expect(capturedIframes[1].contentDocument?.textContent).toBe("Two"); + expect(capturedIframes[1].pseudoClassIds).toEqual({ ":focus": [20] }); }); }); diff --git a/packages/playwright/src/takeSnapshot.ts b/packages/playwright/src/takeSnapshot.ts index 787f882c..caa6c20c 100644 --- a/packages/playwright/src/takeSnapshot.ts +++ b/packages/playwright/src/takeSnapshot.ts @@ -1,14 +1,16 @@ -import { readFileSync } from 'node:fs'; -import { createRequire } from 'node:module'; -import type { Frame, Page, TestInfo } from '@playwright/test'; -import { NodeType, type serializedNodeWithId } from '@rrweb/types'; -import { type DOMSnapshots, type SerializedIframeNode, logger } from '@chromatic-com/shared-e2e'; -import type { takeSnapshot as browserTakeSnapshot } from './browser'; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; -const browserEntry = createRequire(import.meta.url).resolve('@chromatic-com/playwright/browser'); -const browserScript = readFileSync(browserEntry, 'utf-8'); +import { type DOMSnapshots, type SerializedIframeNode, logger } from "@chromatic-com/shared-e2e"; +import type { Frame, Page, TestInfo } from "@playwright/test"; +import { NodeType, type serializedNodeWithId } from "@rrweb/types"; -type TestID = TestInfo['testId']; +import type { takeSnapshot as browserTakeSnapshot } from "./browser"; + +const browserEntry = createRequire(import.meta.url).resolve("@chromatic-com/playwright/browser"); +const browserScript = readFileSync(browserEntry, "utf-8"); + +type TestID = TestInfo["testId"]; type SnapshotName = keyof DOMSnapshots; export const chromaticSnapshots: Map< TestID, @@ -20,12 +22,12 @@ async function takeSnapshot(page: Page, name: string, testInfo: TestInfo): Promi async function takeSnapshot( page: Page, nameOrTestInfo: string | TestInfo, - maybeTestInfo?: TestInfo + maybeTestInfo?: TestInfo, ): Promise { let name: string; let testId: string; - if (typeof nameOrTestInfo === 'string') { - if (!maybeTestInfo) throw new Error('Incorrect usage'); + if (typeof nameOrTestInfo === "string") { + if (!maybeTestInfo) throw new Error("Incorrect usage"); testId = maybeTestInfo.testId; name = nameOrTestInfo; } else { @@ -34,7 +36,7 @@ async function takeSnapshot( name = `Snapshot #${number}`; } - page.on('console', (msg) => { + page.on("console", (msg) => { logger.log(`CONSOLE: "${msg.text()}"`); }); @@ -92,7 +94,7 @@ async function executeSnapshotScript(context: Page | Frame) { ${browserScript} return JSON.stringify(await takeSnapshot()); - }()` + }()`, ); // Additional JSON.stringify + parse needed for deeply nested DOMs: https://github.com/chromaui/chromatic-e2e/issues/307 @@ -100,13 +102,13 @@ async function executeSnapshotScript(context: Page | Frame) { } function findIframes( - node: serializedNodeWithId -): (serializedNodeWithId & { type: NodeType.Element; tagName: 'iframe' })[] { - if (node.type === NodeType.Element && node.tagName === 'iframe') { - return [node as serializedNodeWithId & { type: NodeType.Element; tagName: 'iframe' }]; + node: serializedNodeWithId, +): (serializedNodeWithId & { type: NodeType.Element; tagName: "iframe" })[] { + if (node.type === NodeType.Element && node.tagName === "iframe") { + return [node as serializedNodeWithId & { type: NodeType.Element; tagName: "iframe" }]; } - if ('childNodes' in node) { + if ("childNodes" in node) { return node.childNodes.flatMap((childNode) => { return findIframes(childNode); }); @@ -118,7 +120,7 @@ function findIframes( function filterIframes( nodes: SerializedIframeNode[], url: string, - usedIndexes: Set + usedIndexes: Set, ): SerializedIframeNode | undefined { return nodes.find((node, index) => { if (usedIndexes.has(index)) { diff --git a/packages/playwright/src/types.test-d.ts b/packages/playwright/src/types.test-d.ts index 50b978b5..d7005594 100644 --- a/packages/playwright/src/types.test-d.ts +++ b/packages/playwright/src/types.test-d.ts @@ -1,25 +1,25 @@ -import { expectTypeOf, test } from 'vitest'; -import { Page, TestInfo } from '@playwright/test'; +import { Page, TestInfo } from "@playwright/test"; +import { expectTypeOf, test } from "vitest"; -import * as publicAPI from '../dist/index.mjs'; +import * as publicAPI from "../dist/index.mjs"; -test('test.use() chromatic options', () => { - expectTypeOf(publicAPI.test.use).parameter(0).not.toHaveProperty('somethingThatDoesNotExist'); +test("test.use() chromatic options", () => { + expectTypeOf(publicAPI.test.use).parameter(0).not.toHaveProperty("somethingThatDoesNotExist"); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('delay'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('diffIncludeAntiAliasing'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('diffThreshold'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('disableAutoSnapshot'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('forcedColors'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('pauseAnimationAtEnd'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('prefersReducedMotion'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('resourceArchiveTimeout'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('assetDomains'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('cropToViewport'); - expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty('ignoreSelectors'); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("delay"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("diffIncludeAntiAliasing"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("diffThreshold"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("disableAutoSnapshot"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("forcedColors"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("pauseAnimationAtEnd"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("prefersReducedMotion"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("resourceArchiveTimeout"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("assetDomains"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("cropToViewport"); + expectTypeOf(publicAPI.test.use).parameter(0).toHaveProperty("ignoreSelectors"); }); -test('takeSnapshot', () => { +test("takeSnapshot", () => { expectTypeOf(publicAPI.takeSnapshot).toBeFunction(); expectTypeOf(publicAPI.takeSnapshot).parameters.toEqualTypeOf< diff --git a/packages/playwright/tests/amd.spec.ts b/packages/playwright/tests/amd.spec.ts index 88964a21..6a2c6647 100644 --- a/packages/playwright/tests/amd.spec.ts +++ b/packages/playwright/tests/amd.spec.ts @@ -1,19 +1,20 @@ -import * as path from 'path'; -import { test, expect } from '../src'; +import * as path from "node:path"; -test.use({ ignoreSelectors: ['#objectUrl'] }); -test('pages with AMD modules are archived', async ({ page }) => { - await page.goto('/amd'); - await expect(page.getByText('Sum of')).toBeVisible(); +import { test, expect } from "../src"; + +test.use({ ignoreSelectors: ["#objectUrl"] }); +test("pages with AMD modules are archived", async ({ page }) => { + await page.goto("/amd"); + await expect(page.getByText("Sum of")).toBeVisible(); // why are we testing blob URL stuff here? Because with AMD, // we need to call this blob stuff separately than non-AMD - const fileWithPath = path.join(import.meta.dirname, '../../../test-server/fixtures/blue.png'); + const fileWithPath = path.join(import.meta.dirname, "../../../test-server/fixtures/blue.png"); const [fileChooser] = await Promise.all([ - page.waitForEvent('filechooser'), - page.locator('#fileInput').click(), + page.waitForEvent("filechooser"), + page.locator("#fileInput").click(), ]); await fileChooser.setFiles([fileWithPath]); - await page.locator('#fileInput').dispatchEvent('change'); - await expect(page.locator('#objectUrl')).toHaveText(/blob:.*/); + await page.locator("#fileInput").dispatchEvent("change"); + await expect(page.locator("#objectUrl")).toHaveText(/blob:.*/); }); diff --git a/packages/playwright/tests/archiving-assets.spec.ts b/packages/playwright/tests/archiving-assets.spec.ts index a274e21b..975ea1a8 100644 --- a/packages/playwright/tests/archiving-assets.spec.ts +++ b/packages/playwright/tests/archiving-assets.spec.ts @@ -1,102 +1,103 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { test } from '../src'; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { test } from "../src"; // domain of external image in test (to archive) -test.use({ assetDomains: ['some.external'] }); +test.use({ assetDomains: ["some.external"] }); -test('query params determine which asset is served', async ({ page }) => { - await page.goto('/asset-paths/query-params'); +test("query params determine which asset is served", async ({ page }) => { + await page.goto("/asset-paths/query-params"); }); -test('asset doesnt prevent directory from being created', async ({ page }) => { - await page.goto('/asset-paths/asset-at-directory-name'); +test("asset doesnt prevent directory from being created", async ({ page }) => { + await page.goto("/asset-paths/asset-at-directory-name"); }); -test('asset is found at relative path', async ({ page }) => { - await page.goto('/asset-paths/relative-path'); +test("asset is found at relative path", async ({ page }) => { + await page.goto("/asset-paths/relative-path"); }); -test('long file names are allowed', async ({ page }) => { - await page.goto('/asset-paths/long-file-name'); +test("long file names are allowed", async ({ page }) => { + await page.goto("/asset-paths/long-file-name"); }); -test('external asset is not archived (but still renders)', async ({ page }) => { - await page.goto('/asset-paths/external-asset-not-archived'); +test("external asset is not archived (but still renders)", async ({ page }) => { + await page.goto("/asset-paths/external-asset-not-archived"); }); -test('external asset is archived', async ({ page }) => { +test("external asset is archived", async ({ page }) => { // mock the external image (which we'll archive) - await page.route('https://some.external/domain/image.png', async (route) => { + await page.route("https://some.external/domain/image.png", async (route) => { const file = await fs.readFile( - path.join(import.meta.dirname, '../../../test-server/fixtures/pink.png'), + path.join(import.meta.dirname, "../../../test-server/fixtures/pink.png"), { - encoding: 'base64', - } + encoding: "base64", + }, ); - await route.fulfill({ body: Buffer.from(file, 'base64') }); + await route.fulfill({ body: Buffer.from(file, "base64") }); }); - await page.goto('/asset-paths/external-asset-archived'); + await page.goto("/asset-paths/external-asset-archived"); }); -test('assets from css urls are archived', async ({ page }) => { - await page.goto('/asset-paths/css-urls'); +test("assets from css urls are archived", async ({ page }) => { + await page.goto("/asset-paths/css-urls"); }); -test('assets from data urls are archived', async ({ page }) => { - await page.goto('/asset-paths/data-urls'); +test("assets from data urls are archived", async ({ page }) => { + await page.goto("/asset-paths/data-urls"); }); -test('percents in URLs are handled', async ({ page }) => { - await page.goto('/asset-paths/percents'); +test("percents in URLs are handled", async ({ page }) => { + await page.goto("/asset-paths/percents"); }); -test('colons in URLs are handled', async ({ page }) => { - await page.goto('/asset-paths/colons'); +test("colons in URLs are handled", async ({ page }) => { + await page.goto("/asset-paths/colons"); }); -test('srcset is used to determine image asset URL', async ({ page }) => { - await page.goto('/asset-paths/srcset'); +test("srcset is used to determine image asset URL", async ({ page }) => { + await page.goto("/asset-paths/srcset"); }); -test('picture source is captured, multiple source elements', async ({ page }) => { - await page.goto('/asset-paths/picture'); +test("picture source is captured, multiple source elements", async ({ page }) => { + await page.goto("/asset-paths/picture"); }); -test('picture source is captured, single source with srcset', async ({ page }) => { - await page.goto('/asset-paths/picture-multiple-srcset'); +test("picture source is captured, single source with srcset", async ({ page }) => { + await page.goto("/asset-paths/picture-multiple-srcset"); }); -test('picture captures fallback image', async ({ page }) => { - await page.goto('/asset-paths/picture-no-matching-source'); +test("picture captures fallback image", async ({ page }) => { + await page.goto("/asset-paths/picture-no-matching-source"); }); -test('external CSS files are inlined', async ({ page }) => { - await page.goto('/asset-paths/external-css-files'); +test("external CSS files are inlined", async ({ page }) => { + await page.goto("/asset-paths/external-css-files"); }); -test('video poster image is rendered', async ({ page }) => { - await page.goto('/asset-paths/video-poster'); +test("video poster image is rendered", async ({ page }) => { + await page.goto("/asset-paths/video-poster"); }); -test('link tags for fonts preloads and other things are handled', async ({ page }) => { - await page.goto('/asset-paths/link-tags'); +test("link tags for fonts preloads and other things are handled", async ({ page }) => { + await page.goto("/asset-paths/link-tags"); }); -test('use tags for sprites are archived', async ({ page }) => { - await page.goto('/asset-paths/sprites'); +test("use tags for sprites are archived", async ({ page }) => { + await page.goto("/asset-paths/sprites"); }); -test('assets from relative css urls with base tag are archived', async ({ page }) => { - await page.goto('/asset-paths/relative-urls-with-base'); +test("assets from relative css urls with base tag are archived", async ({ page }) => { + await page.goto("/asset-paths/relative-urls-with-base"); }); -test.describe('', () => { +test.describe("", () => { // Test case renders animated .gif test.use({ delay: 2500 }); - test('assets with a query string and extension are archived', async ({ page }) => { - await page.goto('/asset-paths/skew-protection-query'); + test("assets with a query string and extension are archived", async ({ page }) => { + await page.goto("/asset-paths/skew-protection-query"); }); }); diff --git a/packages/playwright/tests/auth.spec.ts b/packages/playwright/tests/auth.spec.ts index f3fb11c3..29f4b600 100644 --- a/packages/playwright/tests/auth.spec.ts +++ b/packages/playwright/tests/auth.spec.ts @@ -1,27 +1,27 @@ -import { test, expect } from '../src'; +import { test, expect } from "../src"; test.describe(() => { - test('can succeed with basic authentication using globally-defined credentials', async ({ + test("can succeed with basic authentication using globally-defined credentials", async ({ page, }) => { - await page.goto('/protected'); + await page.goto("/protected"); - await expect(page.getByText('I AM PROTECTED!!!')).toBeVisible(); + await expect(page.getByText("I AM PROTECTED!!!")).toBeVisible(); }); }); test.describe(() => { test.use({ extraHTTPHeaders: { - Authorization: `Basic ${btoa('admin:supersecret')}`, + Authorization: `Basic ${btoa("admin:supersecret")}`, }, }); - test('can succeed with basic authentication using a locally-defined header that overrides globally-defined credentials', async ({ + test("can succeed with basic authentication using a locally-defined header that overrides globally-defined credentials", async ({ page, }) => { - await page.goto('/admin'); + await page.goto("/admin"); - await expect(page.getByText('I AM PROTECTED!!!')).toBeVisible(); + await expect(page.getByText("I AM PROTECTED!!!")).toBeVisible(); }); }); diff --git a/packages/playwright/tests/canvas.spec.ts b/packages/playwright/tests/canvas.spec.ts index 17926d0a..c898014c 100644 --- a/packages/playwright/tests/canvas.spec.ts +++ b/packages/playwright/tests/canvas.spec.ts @@ -1,5 +1,5 @@ -import { test } from '../src'; +import { test } from "../src"; -test('captures basic canvas elements', async ({ page }) => { - await page.goto('/canvas'); +test("captures basic canvas elements", async ({ page }) => { + await page.goto("/canvas"); }); diff --git a/packages/playwright/tests/color-scheme.spec.ts b/packages/playwright/tests/color-scheme.spec.ts index 70fc3243..d1165677 100644 --- a/packages/playwright/tests/color-scheme.spec.ts +++ b/packages/playwright/tests/color-scheme.spec.ts @@ -1,38 +1,38 @@ -import { expect, takeSnapshot, test } from '../src'; +import { expect, takeSnapshot, test } from "../src"; -test.describe('light color scheme', () => { - test.use({ colorScheme: 'light' }); +test.describe("light color scheme", () => { + test.use({ colorScheme: "light" }); - test('renders the light-only content', async ({ page }) => { - await page.goto('/color-scheme'); + test("renders the light-only content", async ({ page }) => { + await page.goto("/color-scheme"); - await expect(page.getByText('Only visible in LIGHT mode')).toBeVisible(); - await expect(page.getByText('Only visible in DARK mode')).toBeHidden(); + await expect(page.getByText("Only visible in LIGHT mode")).toBeVisible(); + await expect(page.getByText("Only visible in DARK mode")).toBeHidden(); }); }); -test.describe('dark color scheme', () => { - test.use({ colorScheme: 'dark' }); +test.describe("dark color scheme", () => { + test.use({ colorScheme: "dark" }); - test('renders the dark-only content', async ({ page }) => { - await page.goto('/color-scheme'); + test("renders the dark-only content", async ({ page }) => { + await page.goto("/color-scheme"); - await expect(page.getByText('Only visible in DARK mode')).toBeVisible(); - await expect(page.getByText('Only visible in LIGHT mode')).toBeHidden(); + await expect(page.getByText("Only visible in DARK mode")).toBeVisible(); + await expect(page.getByText("Only visible in LIGHT mode")).toBeHidden(); }); }); test.describe(() => { test.use({ disableAutoSnapshot: true }); - test('captures both color schemes inside a single test case', async ({ page }, testInfo) => { - await page.emulateMedia({ colorScheme: 'dark' }); - await page.goto('/color-scheme'); - await expect(page.getByText('Only visible in DARK mode')).toBeVisible(); - await takeSnapshot(page, 'dark', testInfo); + test("captures both color schemes inside a single test case", async ({ page }, testInfo) => { + await page.emulateMedia({ colorScheme: "dark" }); + await page.goto("/color-scheme"); + await expect(page.getByText("Only visible in DARK mode")).toBeVisible(); + await takeSnapshot(page, "dark", testInfo); - await page.emulateMedia({ colorScheme: 'light' }); - await expect(page.getByText('Only visible in LIGHT mode')).toBeVisible(); - await takeSnapshot(page, 'light', testInfo); + await page.emulateMedia({ colorScheme: "light" }); + await expect(page.getByText("Only visible in LIGHT mode")).toBeVisible(); + await takeSnapshot(page, "light", testInfo); }); }); diff --git a/packages/playwright/tests/constructable-stylesheets.spec.ts b/packages/playwright/tests/constructable-stylesheets.spec.ts index 2d885ecc..672cb948 100644 --- a/packages/playwright/tests/constructable-stylesheets.spec.ts +++ b/packages/playwright/tests/constructable-stylesheets.spec.ts @@ -1,17 +1,17 @@ -import { test } from '../src'; +import { test } from "../src"; -test('styles render in plain HTML', async ({ page }) => { - await page.goto('/constructable-stylesheets/plain'); +test("styles render in plain HTML", async ({ page }) => { + await page.goto("/constructable-stylesheets/plain"); }); -test('styles render in shadow DOM elements', async ({ page }) => { - await page.goto('/constructable-stylesheets/shadow-dom'); +test("styles render in shadow DOM elements", async ({ page }) => { + await page.goto("/constructable-stylesheets/shadow-dom"); }); -test('styles render in Web Components', async ({ page }) => { - await page.goto('/constructable-stylesheets/web-components'); +test("styles render in Web Components", async ({ page }) => { + await page.goto("/constructable-stylesheets/web-components"); }); -test('styles render in web components in shadow DOM', async ({ page }) => { - await page.goto('/constructable-stylesheets/web-components-shadow-dom'); +test("styles render in web components in shadow DOM", async ({ page }) => { + await page.goto("/constructable-stylesheets/web-components-shadow-dom"); }); diff --git a/packages/playwright/tests/createObjectUrl.spec.ts b/packages/playwright/tests/createObjectUrl.spec.ts index ec17f0d0..df5b2d42 100644 --- a/packages/playwright/tests/createObjectUrl.spec.ts +++ b/packages/playwright/tests/createObjectUrl.spec.ts @@ -1,23 +1,24 @@ -import * as path from 'path'; -import { test, expect } from '../src'; +import * as path from "node:path"; -test.use({ ignoreSelectors: ['#objectUrl'] }); +import { test, expect } from "../src"; -test('Upload a Single file and Assert blob', async ({ page }) => { - await page.goto('/createObjectUrl'); - const fileWithPath = path.join(import.meta.dirname, '../../../test-server/fixtures/blue.png'); +test.use({ ignoreSelectors: ["#objectUrl"] }); + +test("Upload a Single file and Assert blob", async ({ page }) => { + await page.goto("/createObjectUrl"); + const fileWithPath = path.join(import.meta.dirname, "../../../test-server/fixtures/blue.png"); const [fileChooser] = await Promise.all([ - page.waitForEvent('filechooser'), - page.locator('#fileInput').click(), + page.waitForEvent("filechooser"), + page.locator("#fileInput").click(), ]); await fileChooser.setFiles([fileWithPath]); - await page.locator('#fileInput').dispatchEvent('change'); - await expect(page.locator('#objectUrl')).toHaveText(/blob:.*/); + await page.locator("#fileInput").dispatchEvent("change"); + await expect(page.locator("#objectUrl")).toHaveText(/blob:.*/); }); // adapted from https://fossies.org/linux/playwright/tests/library/trace-viewer.spec.ts -test('Fetch data for blob', async ({ page }) => { - await page.goto('/createObjectUrl?noUpload=true'); - const size = await page.locator('#blobImg').evaluate((e) => (e as HTMLImageElement).naturalWidth); +test("Fetch data for blob", async ({ page }) => { + await page.goto("/createObjectUrl?noUpload=true"); + const size = await page.locator("#blobImg").evaluate((e) => (e as HTMLImageElement).naturalWidth); expect(size).toBe(10); }); diff --git a/packages/playwright/tests/css-pseudo-states.spec.ts b/packages/playwright/tests/css-pseudo-states.spec.ts index b69c5d30..731ae469 100644 --- a/packages/playwright/tests/css-pseudo-states.spec.ts +++ b/packages/playwright/tests/css-pseudo-states.spec.ts @@ -1,49 +1,49 @@ -import { expect, test, takeSnapshot } from '../src'; +import { expect, test, takeSnapshot } from "../src"; test.use({ disableAutoSnapshot: true }); -test('captures :hover state', async ({ page }, testInfo) => { - await page.goto('/css-pseudo-states'); +test("captures :hover state", async ({ page }, testInfo) => { + await page.goto("/css-pseudo-states"); - await page.locator('button#target').hover(); - expect(page.locator('button:hover')).toBeVisible(); + await page.locator("button#target").hover(); + await expect(page.locator("button:hover")).toBeVisible(); - await takeSnapshot(page, 'hover', testInfo); + await takeSnapshot(page, "hover", testInfo); }); -test('captures :focus state', async ({ page }, testInfo) => { - await page.goto('/css-pseudo-states'); +test("captures :focus state", async ({ page }, testInfo) => { + await page.goto("/css-pseudo-states"); - await page.getByRole('button', { name: 'target' }).click(); - expect(page.locator('button:focus')).toBeVisible(); + await page.getByRole("button", { name: "target" }).click(); + await expect(page.locator("button:focus")).toBeVisible(); - await takeSnapshot(page, 'focus', testInfo); + await takeSnapshot(page, "focus", testInfo); }); -test('captures :focus-visible state', async ({ page }, testInfo) => { - await page.goto('/css-pseudo-states'); +test("captures :focus-visible state", async ({ page }, testInfo) => { + await page.goto("/css-pseudo-states"); - await page.getByRole('button', { name: 'Focus this before tab' }).click(); - await page.keyboard.press('Tab'); + await page.getByRole("button", { name: "Focus this before tab" }).click(); + await page.keyboard.press("Tab"); - expect(page.locator('button:focus-visible')).toBeVisible(); + await expect(page.locator("button:focus-visible")).toBeVisible(); - await takeSnapshot(page, 'focus-visible', testInfo); + await takeSnapshot(page, "focus-visible", testInfo); }); -test('captures :active state', async ({ page }, testInfo) => { - await page.goto('/css-pseudo-states'); +test("captures :active state", async ({ page }, testInfo) => { + await page.goto("/css-pseudo-states"); - const box = await page.locator('button#target').boundingBox(); + const box = await page.locator("button#target").boundingBox(); if (!box) { - throw new Error('Cannot find button#target'); + throw new Error("Cannot find button#target"); } await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); - expect(page.locator('button:active')).toBeVisible(); + await expect(page.locator("button:active")).toBeVisible(); - await takeSnapshot(page, 'active', testInfo); + await takeSnapshot(page, "active", testInfo); }); diff --git a/packages/playwright/tests/deeply-nested-dom.spec.ts b/packages/playwright/tests/deeply-nested-dom.spec.ts index bbc27d6b..e94f8b0c 100644 --- a/packages/playwright/tests/deeply-nested-dom.spec.ts +++ b/packages/playwright/tests/deeply-nested-dom.spec.ts @@ -1,5 +1,5 @@ -import { test } from '../src'; +import { test } from "../src"; -test('pages with a deeply nested DOM are archived', async ({ page }) => { - await page.goto('/deeply-nested-dom'); +test("pages with a deeply nested DOM are archived", async ({ page }) => { + await page.goto("/deeply-nested-dom"); }); diff --git a/packages/playwright/tests/duplicate-test-names.spec.ts b/packages/playwright/tests/duplicate-test-names.spec.ts index 9719534e..b81c9082 100644 --- a/packages/playwright/tests/duplicate-test-names.spec.ts +++ b/packages/playwright/tests/duplicate-test-names.spec.ts @@ -5,13 +5,13 @@ * So let's only test duplicate snapshot names. */ -import { takeSnapshot, test } from '../src'; +import { takeSnapshot, test } from "../src"; test.use({ disableAutoSnapshot: true }); -test('duplicate snapshot names', async ({ page }, testInfo) => { - await page.goto('/'); +test("duplicate snapshot names", async ({ page }, testInfo) => { + await page.goto("/"); - await takeSnapshot(page, 'example', testInfo); - await takeSnapshot(page, 'example', testInfo); + await takeSnapshot(page, "example", testInfo); + await takeSnapshot(page, "example", testInfo); }); diff --git a/packages/playwright/tests/embeds.spec.ts b/packages/playwright/tests/embeds.spec.ts index 8b5615c8..a661edf5 100644 --- a/packages/playwright/tests/embeds.spec.ts +++ b/packages/playwright/tests/embeds.spec.ts @@ -1,73 +1,73 @@ -import { test, expect, takeSnapshot } from '../src'; +import { test, expect, takeSnapshot } from "../src"; -test('same-origin embed page loads', async ({ page }) => { - await page.goto('/embeds/same-origin'); - await expect(page.getByRole('heading', { name: 'Embeds' })).toBeVisible(); +test("same-origin embed page loads", async ({ page }) => { + await page.goto("/embeds/same-origin"); + await expect(page.getByRole("heading", { name: "Embeds" })).toBeVisible(); await expect(page.locator('iframe[title="Same-origin iframe"]')).toBeVisible(); }); -test.describe('', () => { - test.use({ assetDomains: ['http://localhost:3001'] }); +test.describe("", () => { + test.use({ assetDomains: ["http://localhost:3001"] }); - test('cross-origin embed page loads', async ({ page }) => { - await page.goto('/embeds/cross-origin'); - await expect(page.getByRole('heading', { name: 'Embeds' })).toBeVisible(); + test("cross-origin embed page loads", async ({ page }) => { + await page.goto("/embeds/cross-origin"); + await expect(page.getByRole("heading", { name: "Embeds" })).toBeVisible(); await expect(page.locator('iframe[title="Cross-origin iframe"]')).toBeVisible(); await expect( - page.frameLocator('iframe[title="Cross-origin iframe"]').getByText('Embedded page') + page.frameLocator('iframe[title="Cross-origin iframe"]').getByText("Embedded page"), ).toBeVisible(); }); - test('nested iframes', async ({ page }) => { - await page.goto('/embeds/nested-pages'); - await expect(page.getByRole('heading', { name: 'Top page' })).toBeVisible(); + test("nested iframes", async ({ page }) => { + await page.goto("/embeds/nested-pages"); + await expect(page.getByRole("heading", { name: "Top page" })).toBeVisible(); await expect( - page.frameLocator('iframe[title="First iframe"]').getByText('Embeds') + page.frameLocator('iframe[title="First iframe"]').getByText("Embeds"), ).toBeVisible(); await expect( - page.frameLocator('iframe[title="Second iframe"]').getByText('Embeds') + page.frameLocator('iframe[title="Second iframe"]').getByText("Embeds"), ).toBeVisible(); }); }); -test.describe('', () => { +test.describe("", () => { // since we're manually taking snapshots after each button click we don't need another snapshot at the end // (and we certainly don't want a snapshot with an autogenerated name) - test.use({ disableAutoSnapshot: true, assetDomains: ['http://localhost:3001'] }); + test.use({ disableAutoSnapshot: true, assetDomains: ["http://localhost:3001"] }); - for (const origin of ['same-origin', 'cross-origin'] as const) { + for (const origin of ["same-origin", "cross-origin"] as const) { test(`embedded page background color can be changed (${origin})`, async ({ page, }, testInfo) => { await page.goto(`/embeds/${origin}`); - await expect(page.getByRole('heading', { name: 'Embeds' })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Embeds" })).toBeVisible(); - const title = origin === 'same-origin' ? 'Same-origin' : 'Cross-origin'; + const title = origin === "same-origin" ? "Same-origin" : "Cross-origin"; const embedded = page.frameLocator(`iframe[title="${title} iframe"]`); - await expect(embedded.getByRole('heading', { name: 'Embedded page' })).toBeVisible(); + await expect(embedded.getByRole("heading", { name: "Embedded page" })).toBeVisible(); - await embedded.getByRole('button', { name: 'Red' }).click(); - await expect(embedded.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); - await takeSnapshot(page, 'Red background', testInfo); + await embedded.getByRole("button", { name: "Red" }).click(); + await expect(embedded.locator("body")).toHaveCSS("background-color", "rgb(255, 0, 0)"); + await takeSnapshot(page, "Red background", testInfo); - await embedded.getByRole('button', { name: 'Yellow' }).click(); - await expect(embedded.locator('body')).toHaveCSS('background-color', 'rgb(255, 255, 0)'); - await takeSnapshot(page, 'Yellow background', testInfo); + await embedded.getByRole("button", { name: "Yellow" }).click(); + await expect(embedded.locator("body")).toHaveCSS("background-color", "rgb(255, 255, 0)"); + await takeSnapshot(page, "Yellow background", testInfo); - await embedded.getByRole('button', { name: 'Blue' }).click(); - await expect(embedded.locator('body')).toHaveCSS('background-color', 'rgb(0, 0, 255)'); - await takeSnapshot(page, 'Blue background', testInfo); + await embedded.getByRole("button", { name: "Blue" }).click(); + await expect(embedded.locator("body")).toHaveCSS("background-color", "rgb(0, 0, 255)"); + await takeSnapshot(page, "Blue background", testInfo); - await embedded.getByRole('button', { name: 'Reset' }).click(); - await expect(embedded.locator('body')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); - await takeSnapshot(page, 'Reset background', testInfo); + await embedded.getByRole("button", { name: "Reset" }).click(); + await expect(embedded.locator("body")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + await takeSnapshot(page, "Reset background", testInfo); - await embedded.getByRole('button', { name: 'Green on Hover' }).hover(); - await expect(embedded.locator('body')).toHaveCSS('background-color', 'rgb(0, 128, 0)'); - await takeSnapshot(page, 'Green background on hover', testInfo); + await embedded.getByRole("button", { name: "Green on Hover" }).hover(); + await expect(embedded.locator("body")).toHaveCSS("background-color", "rgb(0, 128, 0)"); + await takeSnapshot(page, "Green background on hover", testInfo); }); } }); diff --git a/packages/playwright/tests/forms.spec.ts b/packages/playwright/tests/forms.spec.ts index 0983dac0..1e5720ba 100644 --- a/packages/playwright/tests/forms.spec.ts +++ b/packages/playwright/tests/forms.spec.ts @@ -1,7 +1,7 @@ -import { test, expect } from '../src'; +import { test, expect } from "../src"; -test('form submits successfully', async ({ page }) => { - await page.goto('/forms'); +test("form submits successfully", async ({ page }) => { + await page.goto("/forms"); await page.locator('#form-success input[type="submit"]').click(); - await expect(page.getByText('OK!')).toBeVisible(); + await expect(page.getByText("OK!")).toBeVisible(); }); diff --git a/packages/playwright/tests/ignored-regions.spec.ts b/packages/playwright/tests/ignored-regions.spec.ts index 1807bdf8..329da16b 100644 --- a/packages/playwright/tests/ignored-regions.spec.ts +++ b/packages/playwright/tests/ignored-regions.spec.ts @@ -1,12 +1,12 @@ -import { test, expect } from '../src'; +import { test } from "../src"; // NOTE: This is a test that is meant to be run through Chromatic, so it doesn't actually work // with the automated test suite. test.describe(() => { - test.use({ ignoreSelectors: ['.custom-ignore'] }); - test('ignored regions work with chromatic', async ({ page }) => { + test.use({ ignoreSelectors: [".custom-ignore"] }); + test("ignored regions work with chromatic", async ({ page }) => { test.setTimeout(2000); - await page.goto('/ignore'); - await setTimeout(() => {}, 1000); + await page.goto("/ignore"); + await new Promise((resolve) => setTimeout(resolve, 1000)); }); }); diff --git a/packages/playwright/tests/javascript-disabled.spec.ts b/packages/playwright/tests/javascript-disabled.spec.ts index 3b47ea0b..a12f1d32 100644 --- a/packages/playwright/tests/javascript-disabled.spec.ts +++ b/packages/playwright/tests/javascript-disabled.spec.ts @@ -1,10 +1,10 @@ -import { takeSnapshot, test } from '../src'; +import { takeSnapshot, test } from "../src"; -test.describe('', () => { +test.describe("", () => { test.use({ javaScriptEnabled: false, disableAutoSnapshot: true }); - test('visiting page while JavaScript disabled', async ({ page }, testInfo) => { - await page.goto('/'); + test("visiting page while JavaScript disabled", async ({ page }, testInfo) => { + await page.goto("/"); await takeSnapshot(page, testInfo); }); diff --git a/packages/playwright/tests/long-test-names.spec.ts b/packages/playwright/tests/long-test-names.spec.ts index 42344bcc..e7720808 100644 --- a/packages/playwright/tests/long-test-names.spec.ts +++ b/packages/playwright/tests/long-test-names.spec.ts @@ -1,21 +1,21 @@ -import { test } from '../src'; +import { test } from "../src"; -test.describe('this is a very long story name it just keeps going and going and it cannot stop and it will not stop ba bada da da dum dum dum', () => { - test('and this is also an incredibly long test name because there are just a bunch of random chars at the end like this ldlk elke lekj felk felkf lkf lsf lkef lse flskef ls fls eflsj flksef', async ({ +test.describe("this is a very long story name it just keeps going and going and it cannot stop and it will not stop ba bada da da dum dum dum", () => { + test("and this is also an incredibly long test name because there are just a bunch of random chars at the end like this ldlk elke lekj felk felkf lkf lsf lkef lse flskef ls fls eflsj flksef", async ({ page, }) => { - await page.goto('/'); + await page.goto("/"); }); - test('and this is also an incredibly long test name because there are just a bunch of random chars at the end like this ldlk elke lekj felk felkf lkf lsf lkef lse flskef ls fls eflsj flksef 2', async ({ + test("and this is also an incredibly long test name because there are just a bunch of random chars at the end like this ldlk elke lekj felk felkf lkf lsf lkef lse flskef ls fls eflsj flksef 2", async ({ page, }) => { - await page.goto('/'); + await page.goto("/"); }); - test('multi-byte characters test case: ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ', async ({ + test("multi-byte characters test case: ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ、ああだこうだ", async ({ page, }) => { - await page.goto('/'); + await page.goto("/"); }); }); diff --git a/packages/playwright/tests/manual-snapshots.spec.ts b/packages/playwright/tests/manual-snapshots.spec.ts index 33d9b8f2..58f63e5e 100644 --- a/packages/playwright/tests/manual-snapshots.spec.ts +++ b/packages/playwright/tests/manual-snapshots.spec.ts @@ -1,29 +1,29 @@ -import { test, expect, takeSnapshot } from '../src'; +import { test, expect, takeSnapshot } from "../src"; -test('snapshot names increment for both manual and automatic', async ({ page }, testInfo) => { - await page.goto('/manual-snapshots'); +test("snapshot names increment for both manual and automatic", async ({ page }, testInfo) => { + await page.goto("/manual-snapshots"); await takeSnapshot(page, testInfo); - await page.locator('summary').click(); - await expect(page.getByText('I am hiding inside!')).toBeVisible(); - await takeSnapshot(page, 'another test', testInfo); + await page.locator("summary").click(); + await expect(page.getByText("I am hiding inside!")).toBeVisible(); + await takeSnapshot(page, "another test", testInfo); await takeSnapshot(page, testInfo); }); -test.describe('', () => { +test.describe("", () => { test.use({ disableAutoSnapshot: true }); - test('manual snapshot is taken even when automatic snapshots are turned off', async ({ + test("manual snapshot is taken even when automatic snapshots are turned off", async ({ page, }, testInfo) => { - await page.goto('/manual-snapshots'); - await takeSnapshot(page, 'accordion collapsed', testInfo); - await page.locator('summary').click(); - await expect(page.getByText('I am hiding inside!')).toBeVisible(); + await page.goto("/manual-snapshots"); + await takeSnapshot(page, "accordion collapsed", testInfo); + await page.locator("summary").click(); + await expect(page.getByText("I am hiding inside!")).toBeVisible(); }); - test('manual snapshot name autogenerated when not passed', async ({ page }, testInfo) => { - await page.goto('/manual-snapshots'); + test("manual snapshot name autogenerated when not passed", async ({ page }, testInfo) => { + await page.goto("/manual-snapshots"); await takeSnapshot(page, testInfo); - await page.locator('summary').click(); - await expect(page.getByText('I am hiding inside!')).toBeVisible(); + await page.locator("summary").click(); + await expect(page.getByText("I am hiding inside!")).toBeVisible(); }); }); diff --git a/packages/playwright/tests/newline-test-names.spec.ts b/packages/playwright/tests/newline-test-names.spec.ts index a2f9b687..c1d5db9a 100644 --- a/packages/playwright/tests/newline-test-names.spec.ts +++ b/packages/playwright/tests/newline-test-names.spec.ts @@ -1,4 +1,4 @@ -import { takeSnapshot, test } from '../src'; +import { takeSnapshot, test } from "../src"; test.describe(` Test @@ -7,13 +7,13 @@ newlines `, () => { test.use({ disableAutoSnapshot: true }); - test('Are\n\rRemoved\r\nFrom\nFile\rNames\n\n\r\r', async ({ page }, testInfo) => { - await page.goto('/'); + test("Are\n\rRemoved\r\nFrom\nFile\rNames\n\n\r\r", async ({ page }, testInfo) => { + await page.goto("/"); await takeSnapshot(page, testInfo); }); - test('newlines in snapshot name', async ({ page }, testInfo) => { - await page.goto('/'); - await takeSnapshot(page, 'snapshot name\nwith newlines\r\nand carriage returns', testInfo); + test("newlines in snapshot name", async ({ page }, testInfo) => { + await page.goto("/"); + await takeSnapshot(page, "snapshot name\nwith newlines\r\nand carriage returns", testInfo); }); }); diff --git a/packages/playwright/tests/no-doctype.spec.ts b/packages/playwright/tests/no-doctype.spec.ts index a4b7d67a..9541beb5 100644 --- a/packages/playwright/tests/no-doctype.spec.ts +++ b/packages/playwright/tests/no-doctype.spec.ts @@ -1,5 +1,5 @@ -import { test } from '../src'; +import { test } from "../src"; -test('pages without a doctype are archived', async ({ page }) => { - await page.goto('/no-doctype'); +test("pages without a doctype are archived", async ({ page }) => { + await page.goto("/no-doctype"); }); diff --git a/packages/playwright/tests/options.spec.ts b/packages/playwright/tests/options.spec.ts index c6c30681..95be76d9 100644 --- a/packages/playwright/tests/options.spec.ts +++ b/packages/playwright/tests/options.spec.ts @@ -1,53 +1,53 @@ -import { test } from '../src'; +import { test } from "../src"; test.describe(() => { test.use({ delay: 2500 }); - test('delay', async ({ page }) => { - await page.goto('/options/delay'); + test("delay", async ({ page }) => { + await page.goto("/options/delay"); }); }); test.describe(() => { test.use({ diffThreshold: 1 }); - test('diff threshold', async ({ page }) => { - await page.goto('/options/diff-threshold'); + test("diff threshold", async ({ page }) => { + await page.goto("/options/diff-threshold"); }); }); test.describe(() => { test.use({ pauseAnimationAtEnd: true }); - test('pause animation at end', async ({ page }) => { - await page.goto('/options/pause-animation-at-end'); + test("pause animation at end", async ({ page }) => { + await page.goto("/options/pause-animation-at-end"); }); }); test.describe(() => { - test.use({ forcedColors: 'active' }); + test.use({ forcedColors: "active" }); - test('force high-contrast', async ({ page }) => { - await page.goto('/options/forced-colors'); + test("force high-contrast", async ({ page }) => { + await page.goto("/options/forced-colors"); }); }); test.describe(() => { - test.use({ prefersReducedMotion: 'reduce' }); + test.use({ prefersReducedMotion: "reduce" }); - test('prefers reduced motion', async ({ page }) => { - await page.goto('/options/prefers-reduced-motion'); + test("prefers reduced motion", async ({ page }) => { + await page.goto("/options/prefers-reduced-motion"); }); }); test.describe(() => { test.use({ cropToViewport: true }); - test('crops to viewport', async ({ page }) => { - await page.goto('/options/crop-to-viewport'); + test("crops to viewport", async ({ page }) => { + await page.goto("/options/crop-to-viewport"); }); }); -test('does not crop to viewport by default', async ({ page }) => { - await page.goto('/options/crop-to-viewport'); +test("does not crop to viewport by default", async ({ page }) => { + await page.goto("/options/crop-to-viewport"); }); diff --git a/packages/playwright/tests/snapshot-names.spec.ts b/packages/playwright/tests/snapshot-names.spec.ts index bd9f4a5a..f209f25c 100644 --- a/packages/playwright/tests/snapshot-names.spec.ts +++ b/packages/playwright/tests/snapshot-names.spec.ts @@ -1,16 +1,16 @@ -import { test, takeSnapshot } from '../src'; +import { test, takeSnapshot } from "../src"; -test.describe('', () => { +test.describe("", () => { test.use({ disableAutoSnapshot: true }); - test('in snapshot name', async ({ page }, testInfo) => { - await page.goto('/'); + test("in snapshot name", async ({ page }, testInfo) => { + await page.goto("/"); - await takeSnapshot(page, 'あ', testInfo); + await takeSnapshot(page, "あ", testInfo); }); - test('in test case name あ', async ({ page }, testInfo) => { - await page.goto('/'); + test("in test case name あ", async ({ page }, testInfo) => { + await page.goto("/"); await takeSnapshot(page, testInfo); }); diff --git a/packages/playwright/tests/storybook.spec.ts b/packages/playwright/tests/storybook.spec.ts index fc8dc2e2..7991c92f 100644 --- a/packages/playwright/tests/storybook.spec.ts +++ b/packages/playwright/tests/storybook.spec.ts @@ -1,9 +1,9 @@ -import { test, expect } from '../src'; +import { test, expect } from "../src"; -test.use({ assetDomains: ['localhost:6006'] }); -test.skip('visits hosted storybook page', async ({ page }) => { +test.use({ assetDomains: ["localhost:6006"] }); +test.skip("visits hosted storybook page", async ({ page }) => { await page.goto( - 'https://main--653fef099b8957739e7534a4.chromatic.com/iframe.html?id=options-pause-animation-at-end--snapshot-1&viewMode=story' + "https://main--653fef099b8957739e7534a4.chromatic.com/iframe.html?id=options-pause-animation-at-end--snapshot-1&viewMode=story", ); - await expect(page.getByRole('heading', { name: /The image/i })).toBeVisible(); + await expect(page.getByRole("heading", { name: /The image/i })).toBeVisible(); }); diff --git a/packages/playwright/tests/viewports.spec.ts b/packages/playwright/tests/viewports.spec.ts index 7edd22ac..b4a074a7 100644 --- a/packages/playwright/tests/viewports.spec.ts +++ b/packages/playwright/tests/viewports.spec.ts @@ -1,40 +1,40 @@ -import { expect, takeSnapshot, test } from '../src'; +import { expect, takeSnapshot, test } from "../src"; -test('snapshots capture the correct viewport size', async ({ page }) => { - await page.goto('/viewports'); +test("snapshots capture the correct viewport size", async ({ page }) => { + await page.goto("/viewports"); }); -test.describe('hardcoded viewport', () => { +test.describe("hardcoded viewport", () => { test.use({ viewport: { width: 800, height: 720 } }); - test('snapshots capture the correct viewport size', async ({ page }) => { - await page.goto('/viewports'); + test("snapshots capture the correct viewport size", async ({ page }) => { + await page.goto("/viewports"); }); - test('snapshots capture multiple viewports inside a single test case', async ({ + test("snapshots capture multiple viewports inside a single test case", async ({ page, }, testInfo) => { - await page.goto('/viewports'); + await page.goto("/viewports"); await expect(page.getByText("I'm always rendered")).toBeVisible(); - await takeSnapshot(page, 'default', testInfo); + await takeSnapshot(page, "default", testInfo); await page.setViewportSize({ width: 480, height: 320 }); - await expect(page.getByText('Window width: 480')).toBeVisible(); + await expect(page.getByText("Window width: 480")).toBeVisible(); await expect(page.getByText("I'm rendered when the page width is between 500-0")).toBeVisible(); - await takeSnapshot(page, '480 x 320', testInfo); + await takeSnapshot(page, "480 x 320", testInfo); await page.setViewportSize({ width: 850, height: 500 }); - await expect(page.getByText('Window width: 850')).toBeVisible(); + await expect(page.getByText("Window width: 850")).toBeVisible(); await expect( - page.getByText("I'm rendered when the page width is between 900-800") + page.getByText("I'm rendered when the page width is between 900-800"), ).toBeVisible(); - await takeSnapshot(page, '850 x 500', testInfo); + await takeSnapshot(page, "850 x 500", testInfo); await page.setViewportSize({ width: 1050, height: 1080 }); - await expect(page.getByText('Window width: 1050')).toBeVisible(); + await expect(page.getByText("Window width: 1050")).toBeVisible(); await expect( - page.getByText("I'm rendered when the page width is between 1100-1000") + page.getByText("I'm rendered when the page width is between 1100-1000"), ).toBeVisible(); - await takeSnapshot(page, '1050 x 1080', testInfo); + await takeSnapshot(page, "1050 x 1080", testInfo); }); }); diff --git a/packages/playwright/tsdown.config.ts b/packages/playwright/tsdown.config.ts index 231146e3..daf3e29f 100644 --- a/packages/playwright/tsdown.config.ts +++ b/packages/playwright/tsdown.config.ts @@ -1,38 +1,42 @@ -import { defineConfig, type UserConfig } from 'tsdown'; +import { defineConfig, type UserConfig } from "tsdown"; -const defaults: UserConfig = { - format: ['esm'], - platform: 'node', - target: 'esnext', - tsconfig: './tsconfig.json', +const defaults = { + format: ["esm"], + platform: "node", + target: "esnext", + tsconfig: "./tsconfig.json", outputOptions: { comments: false }, minify: false, clean: true, fixedExtension: true, -}; +} satisfies UserConfig; export default defineConfig([ { ...defaults, - name: 'Node', + name: "Node", entry: { - index: 'src/index.ts', - 'bin/archive-storybook': 'src/bin/archive-storybook.ts', - 'bin/build-archive-storybook': 'src/bin/build-archive-storybook.ts', - 'storybook-config/main': 'src/storybook-config/main.ts', + index: "src/index.ts", + "bin/archive-storybook": "src/bin/archive-storybook.ts", + "bin/build-archive-storybook": "src/bin/build-archive-storybook.ts", + "storybook-config/main": "src/storybook-config/main.ts", }, - dts: { entry: ['src/index.ts'] }, - deps: { onlyBundle: ['@rrweb/types', 'mime', 'srcset'] }, + dts: { + entry: ["src/index.ts"], + // tsgolint rejects `baseUrl` in tsconfig files, but tsdown's dts type inference needs it: + compilerOptions: { baseUrl: "../.." }, + }, + deps: { onlyBundle: ["@rrweb/types", "mime", "srcset"] }, }, { ...defaults, - name: 'Browser', + name: "Browser", entry: { - 'storybook-config/manager': 'src/storybook-config/shared/manager.ts', - 'storybook-config/preview': 'src/storybook-config/shared/preview.ts', + "storybook-config/manager": "src/storybook-config/shared/manager.ts", + "storybook-config/preview": "src/storybook-config/shared/preview.ts", }, - platform: 'browser', - deps: { onlyBundle: ['@rrweb/types'] }, + platform: "browser", + deps: { onlyBundle: ["@rrweb/types"] }, }, /** @@ -41,13 +45,13 @@ export default defineConfig([ */ { ...defaults, - name: 'Browser Playwright Script', - entry: { browser: 'src/browser.ts' }, - platform: 'browser', + name: "Browser Playwright Script", + entry: { browser: "src/browser.ts" }, + platform: "browser", outputOptions: { ...defaults.outputOptions, codeSplitting: false }, deps: { - alwaysBundle: ['@chromaui/rrweb-snapshot'], - onlyBundle: ['@chromaui/rrweb-snapshot', '@rrweb/types'], + alwaysBundle: ["@chromaui/rrweb-snapshot"], + onlyBundle: ["@chromaui/rrweb-snapshot", "@rrweb/types"], }, }, ]); diff --git a/packages/playwright/vitest.config.ts b/packages/playwright/vitest.config.ts index ded50d59..4b3fdbae 100644 --- a/packages/playwright/vitest.config.ts +++ b/packages/playwright/vitest.config.ts @@ -1,13 +1,13 @@ -import { defineProject } from 'vitest/config'; +import { defineProject } from "vitest/config"; export default defineProject({ resolve: { tsconfigPaths: true }, test: { - name: { label: 'Playwright', color: 'blue' }, - include: ['src/**/*.test.ts', 'embedded.test.ts'], + name: { label: "Playwright", color: "blue" }, + include: ["src/**/*.test.ts", "embedded.test.ts"], typecheck: { enabled: true, - include: ['src/*.test-d.ts'], + include: ["src/*.test-d.ts"], ignoreSourceErrors: true, }, }, diff --git a/packages/shared/package.json b/packages/shared/package.json index 76cd400e..dd2983f0 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -3,17 +3,14 @@ "version": "0.11.6", "private": true, "description": "Shared Chromatic E2E", + "license": "MIT", + "author": "Chromatic ", "repository": { "type": "git", "url": "https://github.com/chromaui/chromatic-e2e.git", "directory": "packages/shared" }, - "license": "MIT", - "author": "Chromatic ", "type": "module", - "engines": { - "node": ">=22" - }, "exports": { ".": "./src/index", "./test-utils": "./src/test-utils" @@ -43,5 +40,8 @@ "srcset": "^4.0.0", "ts-dedent": "^2.2.0", "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22" } } diff --git a/packages/shared/src/archive-storybook/index.ts b/packages/shared/src/archive-storybook/index.ts index 6f7e80f9..decf705b 100644 --- a/packages/shared/src/archive-storybook/index.ts +++ b/packages/shared/src/archive-storybook/index.ts @@ -1,8 +1,9 @@ -import { spawn } from 'node:child_process'; -import { resolve, dirname } from 'node:path'; -import { readFileSync } from 'node:fs'; -import { createRequire } from 'node:module'; -import { checkArchivesDirExists } from '../utils/filePaths'; +import { spawn } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { resolve, dirname } from "node:path"; + +import { checkArchivesDirExists } from "../utils/filePaths"; interface Callbacks { onArchivesCheck: (error?: unknown) => void; @@ -14,22 +15,22 @@ export async function archiveStorybook( processArgs: string[], configDir: string, defaultOutputDir: string, - callbacks?: Callbacks + callbacks?: Callbacks, ) { assertOutputDirectory(defaultOutputDir, callbacks); - await runBin([binPath(), 'dev', ...processArgs, '-c', configDir]); + await runBin([binPath(), "dev", ...processArgs, "-c", configDir]); } export async function buildArchiveStorybook( processArgs: string[], configDir: string, defaultOutputDir: string, - callbacks?: Callbacks + callbacks?: Callbacks, ) { assertOutputDirectory(defaultOutputDir, callbacks); - await runBin([binPath(), 'build', ...processArgs, '-c', configDir]); + await runBin([binPath(), "build", ...processArgs, "-c", configDir]); } function assertOutputDirectory(outputDirectory: string, callbacks?: Callbacks) { @@ -45,17 +46,17 @@ function assertOutputDirectory(outputDirectory: string, callbacks?: Callbacks) { function runBin(args: string[]): Promise { return new Promise((resolvePromise, reject) => { - const child = spawn('node', args, { stdio: 'inherit' }); + const child = spawn("node", args, { stdio: "inherit" }); - child.on('error', reject); - child.on('close', (code, signal) => { - if (code === 0 || signal === 'SIGINT' || signal === 'SIGTERM') { + child.on("error", reject); + child.on("close", (code, signal) => { + if (code === 0 || signal === "SIGINT" || signal === "SIGTERM") { resolvePromise(); } else { reject( Object.assign(new Error(`Storybook exited with ${signal ?? `code ${code}`}`), { status: code, - }) + }), ); } }); @@ -63,8 +64,8 @@ function runBin(args: string[]): Promise { } function binPath() { - const filename = req.resolve('storybook/package.json'); - const packageJson = JSON.parse(readFileSync(filename, 'utf8')); + const filename = req.resolve("storybook/package.json"); + const packageJson = JSON.parse(readFileSync(filename, "utf8")); // reference the entry file based on the package.json `bin` value, since it changed between SB 8.1 and 8.2 return resolve(dirname(filename), packageJson.bin); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 8a1f6076..cb64774e 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,8 +1,8 @@ -export * from './resource-archiver'; -export * from './write-archive'; -export * from './utils/logger'; -export * from './constants'; -export * from './utils/viewport'; +export * from "./resource-archiver"; +export * from "./write-archive"; +export * from "./utils/logger"; +export * from "./constants"; +export * from "./utils/viewport"; export type { ChromaticConfig, @@ -10,4 +10,4 @@ export type { DOMSnapshots, SavedSnapshot, SerializedIframeNode, -} from './types'; +} from "./types"; diff --git a/packages/shared/src/resource-archiver/index.ts b/packages/shared/src/resource-archiver/index.ts index c1f1b6d2..7b25bd04 100644 --- a/packages/shared/src/resource-archiver/index.ts +++ b/packages/shared/src/resource-archiver/index.ts @@ -1,5 +1,6 @@ -import type { Protocol } from 'playwright-core/types/protocol'; -import { logger } from '../utils/logger'; +import type { Protocol } from "playwright-core/types/protocol"; + +import { logger } from "../utils/logger"; export type UrlString = string; @@ -54,13 +55,13 @@ export class ResourceArchiver { /** * Currently active requests */ - private pendingRequests = new Set(); + private pendingRequests = new Set(); constructor( cdpClient: CDPClient, allowedDomains?: string[], httpCredentials?: HttpCredentials, - firstUrl?: URL + firstUrl?: URL, ) { this.client = cdpClient; this.httpCredentials = httpCredentials; @@ -71,7 +72,7 @@ export class ResourceArchiver { // tack on the protocol so we can properly check if requests are cross-origin this.assetDomains = (allowedDomains || []).map((domain) => { - if (domain.startsWith('http')) { + if (domain.startsWith("http")) { return domain; } @@ -81,15 +82,15 @@ export class ResourceArchiver { } async watch() { - this.client.on('Fetch.requestPaused', this.requestPaused); - this.client.on('Fetch.authRequired', this.authRequired); - await this.client.send('Fetch.enable', { handleAuthRequests: true }); + this.client.on("Fetch.requestPaused", this.requestPaused); + this.client.on("Fetch.authRequired", this.authRequired); + await this.client.send("Fetch.enable", { handleAuthRequests: true }); } async unwatch() { - this.client.off?.('Fetch.requestPaused', this.requestPaused); - this.client.off?.('Fetch.authRequired', this.authRequired); - await this.client.send('Fetch.disable'); + this.client.off?.("Fetch.requestPaused", this.requestPaused); + this.client.off?.("Fetch.authRequired", this.authRequired); + await this.client.send("Fetch.disable"); this.pendingRequests.clear(); } @@ -100,22 +101,22 @@ export class ResourceArchiver { async clientSend( request: Protocol.Network.Request, method: T, - params?: Protocol.CommandParameters[T] + params?: Protocol.CommandParameters[T], ): Promise { try { return await this.client.send(method, params); } catch (error) { - logger.log('Client error', request.url, error); + logger.log("Client error", request.url, error); this.archive[request.url] = { error }; return null; } } authRequired = async ({ requestId, request }: Protocol.Fetch.authRequiredPayload) => { - await this.clientSend(request, 'Fetch.continueWithAuth', { + await this.clientSend(request, "Fetch.continueWithAuth", { requestId, authChallengeResponse: { - response: 'ProvideCredentials', + response: "ProvideCredentials", ...this.httpCredentials, }, }); @@ -134,13 +135,13 @@ export class ResourceArchiver { // We only need to capture assets that will render when the DOM snapshot is rendered, // so we only need to handle GET requests. if (!request.method.match(/get/i)) { - await this.clientSend(request, 'Fetch.continueRequest', { requestId }); + await this.clientSend(request, "Fetch.continueRequest", { requestId }); return; } // There's no reponse body for us to archive on 304s if (responseStatusCode === 304) { - await this.clientSend(request, 'Fetch.continueRequest', { requestId }); + await this.clientSend(request, "Fetch.continueRequest", { requestId }); return; } @@ -152,18 +153,18 @@ export class ResourceArchiver { requestUrl.origin === this.firstUrl.origin || this.assetDomains.includes(requestUrl.origin); logger.log( - 'requestPaused', + "requestPaused", requestUrl.toString(), - responseStatusCode || responseErrorReason ? 'response' : 'request', + responseStatusCode || responseErrorReason ? "response" : "request", this.firstUrl.toString(), - isRequestFromAllowedDomain + isRequestFromAllowedDomain, ); // Pausing at response stage with an error, simply ignore // This can be for example when browser network cache serves the request if (responseErrorReason) { logger.log(`Got response error: ${responseErrorReason}`); - await this.clientSend(request, 'Fetch.continueRequest', { requestId }); + await this.clientSend(request, "Fetch.continueRequest", { requestId }); return; } @@ -178,14 +179,14 @@ export class ResourceArchiver { responseHeaders, }, requestUrl, - isRequestFromAllowedDomain + isRequestFromAllowedDomain, ); return; } this.pendingRequests.add(requestId); - await this.clientSend(request, 'Fetch.continueRequest', { + await this.clientSend(request, "Fetch.continueRequest", { requestId, interceptResponse: true, }); @@ -194,23 +195,23 @@ export class ResourceArchiver { private async handleSuccessfulResponse( requestPausedPayload: Pick< Protocol.Fetch.requestPausedPayload, - 'request' | 'requestId' | 'responseStatusCode' | 'responseStatusText' | 'responseHeaders' + "request" | "requestId" | "responseStatusCode" | "responseStatusText" | "responseHeaders" >, requestUrl: URL, - isRequestFromAllowedDomain: boolean + isRequestFromAllowedDomain: boolean, ) { const { request, requestId, responseStatusCode, responseStatusText, responseHeaders } = requestPausedPayload; if ([301, 302, 303, 307, 308].includes(responseStatusCode)) { - await this.clientSend(request, 'Fetch.continueRequest', { + await this.clientSend(request, "Fetch.continueRequest", { requestId, interceptResponse: true, }); return; } - const result = await this.clientSend(request, 'Fetch.getResponseBody', { + const result = await this.clientSend(request, "Fetch.getResponseBody", { requestId, }); // Something has gone wrong and will be logged above @@ -221,13 +222,13 @@ export class ResourceArchiver { // If the Content-Type header is present, let's capture it. const contentTypeHeader: Protocol.Fetch.HeaderEntry = responseHeaders.find( - ({ name }) => name.toLowerCase() === 'content-type' + ({ name }) => name.toLowerCase() === "content-type", ); // No need to capture the response of the top level page request const isFirstRequest = requestUrl.toString() === this.firstUrl.toString(); if (isRequestFromAllowedDomain && !isFirstRequest) { - logger.log('Archiving request', { + logger.log("Archiving request", { url: request.url, statusCode: responseStatusCode, statusText: responseStatusText, @@ -237,11 +238,11 @@ export class ResourceArchiver { this.archive[request.url] = { statusCode: responseStatusCode, statusText: responseStatusText, - body: Buffer.from(body, base64Encoded ? 'base64' : 'utf8'), + body: Buffer.from(body, base64Encoded ? "base64" : "utf8"), contentType: contentTypeHeader?.value, }; } else { - logger.log('Skipping archiving of request', { + logger.log("Skipping archiving of request", { url: request.url, firstUrl: this.firstUrl.toString(), isFirstRequest, @@ -249,6 +250,6 @@ export class ResourceArchiver { }); } - await this.clientSend(request, 'Fetch.continueRequest', { requestId }); + await this.clientSend(request, "Fetch.continueRequest", { requestId }); } } diff --git a/packages/shared/src/test-utils.ts b/packages/shared/src/test-utils.ts index b2ef2128..467c624e 100644 --- a/packages/shared/src/test-utils.ts +++ b/packages/shared/src/test-utils.ts @@ -1,7 +1,7 @@ // v8 ignore file -- @preserve -import { createHash } from 'node:crypto'; -import fs from 'node:fs'; -import path from 'node:path'; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; interface EmbeddedPackage { relDir: string; @@ -11,7 +11,7 @@ interface EmbeddedPackage { } export function collectEmbeddedFiles(packageRoot: string) { - const embeddedRoot = path.join(packageRoot, 'embedded', 'node_modules'); + const embeddedRoot = path.join(packageRoot, "embedded", "node_modules"); return collectPackages(embeddedRoot, embeddedRoot) .sort((a, b) => a.relDir.localeCompare(b.relDir)) @@ -19,33 +19,33 @@ export function collectEmbeddedFiles(packageRoot: string) { } export function generateEmbeddedChecksum(packageRoot: string) { - const embeddedRoot = path.join(packageRoot, 'embedded', 'node_modules'); + const embeddedRoot = path.join(packageRoot, "embedded", "node_modules"); const files = collectFiles(embeddedRoot, embeddedRoot).sort((a, b) => a.rel.localeCompare(b.rel)); - const hash = createHash('sha256'); + const hash = createHash("sha256"); for (const { abs, rel } of files) { hash.update(rel); - hash.update('\0'); + hash.update("\0"); hash.update(fs.readFileSync(abs)); - hash.update('\0'); + hash.update("\0"); } - return { hash: hash.digest('hex'), fileCount: files.length }; + return { hash: hash.digest("hex"), fileCount: files.length }; } function collectPackages( embeddedRoot: string, nodeModulesDir: string, - into: EmbeddedPackage[] = [] + into: EmbeddedPackage[] = [], ): EmbeddedPackage[] { if (!fs.existsSync(nodeModulesDir)) { return into; } for (const entry of fs.readdirSync(nodeModulesDir, { withFileTypes: true })) { - if (entry.name === '.bin' || !entry.isDirectory()) continue; + if (entry.name === ".bin" || !entry.isDirectory()) continue; - if (entry.name.startsWith('@')) { + if (entry.name.startsWith("@")) { const scopeDir = path.join(nodeModulesDir, entry.name); for (const sub of fs.readdirSync(scopeDir, { withFileTypes: true })) { @@ -62,22 +62,22 @@ function collectPackages( } function addPackage(embeddedRoot: string, absDir: string, into: EmbeddedPackage[]): void { - const packageJson = JSON.parse(fs.readFileSync(path.join(absDir, 'package.json'), 'utf8')); + const packageJson = JSON.parse(fs.readFileSync(path.join(absDir, "package.json"), "utf8")); into.push({ - relDir: path.relative(embeddedRoot, absDir).split(path.sep).join('/'), + relDir: path.relative(embeddedRoot, absDir).split(path.sep).join("/"), absDir, name: packageJson.name, version: packageJson.version, }); - collectPackages(embeddedRoot, path.join(absDir, 'node_modules'), into); + collectPackages(embeddedRoot, path.join(absDir, "node_modules"), into); } function collectFiles( embeddedRoot: string, dir: string, - into: { abs: string; rel: string }[] = [] + into: { abs: string; rel: string }[] = [], ) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const abs = path.join(dir, entry.name); @@ -87,7 +87,7 @@ function collectFiles( } else if (entry.isFile()) { into.push({ abs, - rel: path.relative(embeddedRoot, abs).split(path.sep).join('/'), + rel: path.relative(embeddedRoot, abs).split(path.sep).join("/"), }); } } diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index f48af96c..e3f17036 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,5 +1,6 @@ -import { NodeType, serializedNodeWithId } from '@rrweb/types'; -import { Viewport } from './utils/viewport'; +import { NodeType, serializedNodeWithId } from "@rrweb/types"; + +import { Viewport } from "./utils/viewport"; export interface ChromaticConfig { /** https://www.chromatic.com/docs/delay/ */ @@ -45,7 +46,7 @@ export interface ChromaticConfig { export type ChromaticStorybookParameters = Omit< ChromaticConfig, - 'disableAutoSnapshot' | 'resourceArchiveTimeout' | 'assetDomains' + "disableAutoSnapshot" | "resourceArchiveTimeout" | "assetDomains" >; export type DOMSnapshots = Record< @@ -58,11 +59,11 @@ export type DOMSnapshots = Record< viewport: Viewport; /** Color scheme from the exact time the snapshot was taken */ - colorScheme: 'light' | 'dark'; + colorScheme: "light" | "dark"; /** Mapping of pseudo-class names to their corresponding rrweb-snapshot element IDs */ pseudoClassIds: Partial< - Record<':active' | ':focus' | ':focus-visible' | ':hover', serializedNodeWithId['id'][]> + Record<":active" | ":focus" | ":focus-visible" | ":hover", serializedNodeWithId["id"][]> >; /** Optional parameters for the Story JSON */ @@ -73,12 +74,12 @@ export type DOMSnapshots = Record< /** Shape of the snapshot that is written to **the file system** */ export interface SavedSnapshot { snapshot: serializedNodeWithId; - pseudoClassIds: DOMSnapshots[string]['pseudoClassIds']; + pseudoClassIds: DOMSnapshots[string]["pseudoClassIds"]; } export type SerializedIframeNode = serializedNodeWithId & { type: NodeType.Element; - tagName: 'iframe'; + tagName: "iframe"; contentDocument?: serializedNodeWithId; - pseudoClassIds?: DOMSnapshots[string]['pseudoClassIds']; + pseudoClassIds?: DOMSnapshots[string]["pseudoClassIds"]; }; diff --git a/packages/shared/src/utils/filePaths.test.ts b/packages/shared/src/utils/filePaths.test.ts index bec4fcb4..c7910c56 100644 --- a/packages/shared/src/utils/filePaths.test.ts +++ b/packages/shared/src/utils/filePaths.test.ts @@ -1,13 +1,15 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import fsPromises from 'fs/promises'; -import fs from 'fs'; -import { archivesDir, ensureDir, outputFile, outputJSONFile, truncateFileName } from './filePaths'; -import { removeLocalhostFromBaseUrl } from './filePaths'; +import fs from "node:fs"; +import fsPromises from "node:fs/promises"; -vi.mock('fs'); -vi.mock('fs/promises'); +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const currentDir = '/base/dir'; +import { archivesDir, ensureDir, outputFile, outputJSONFile, truncateFileName } from "./filePaths"; +import { removeLocalhostFromBaseUrl } from "./filePaths"; + +vi.mock("fs"); +vi.mock("fs/promises"); + +const currentDir = "/base/dir"; const originalProcess = process; afterEach(() => { @@ -15,7 +17,7 @@ afterEach(() => { vi.resetAllMocks(); }); -describe('default output dir', () => { +describe("default output dir", () => { beforeEach(() => { global.process = { ...originalProcess, @@ -23,99 +25,99 @@ describe('default output dir', () => { }; }); - describe('archivesDir', () => { - it('returns a full path to the archives dir', async () => { - const fullPath = archivesDir('default/dir'); - expect(fullPath).toEqual('/base/dir/default/dir/chromatic-archives'); + describe("archivesDir", () => { + it("returns a full path to the archives dir", async () => { + const fullPath = archivesDir("default/dir"); + expect(fullPath).toEqual("/base/dir/default/dir/chromatic-archives"); }); }); }); -describe('overridden output dir', () => { +describe("overridden output dir", () => { beforeEach(() => { global.process = { ...originalProcess, cwd: () => currentDir, env: { ...originalProcess.env, - CHROMATIC_ARCHIVE_LOCATION: 'overridden/dir', + CHROMATIC_ARCHIVE_LOCATION: "overridden/dir", }, }; }); - describe('archivesDir', () => { - it('returns a full path to the archives dir', async () => { - const fullPath = archivesDir('default/dir'); - expect(fullPath).toEqual('/base/dir/overridden/dir/chromatic-archives'); + describe("archivesDir", () => { + it("returns a full path to the archives dir", async () => { + const fullPath = archivesDir("default/dir"); + expect(fullPath).toEqual("/base/dir/overridden/dir/chromatic-archives"); }); }); }); -describe('ensureDir', () => { - it('creates the directory if it does not exist', async () => { +describe("ensureDir", () => { + it("creates the directory if it does not exist", async () => { vi.mocked(fs.existsSync).mockReturnValueOnce(false); - ensureDir('/some/path'); - expect(fs.mkdirSync).toHaveBeenCalledWith('/some/path', { recursive: true }); + ensureDir("/some/path"); + expect(fs.mkdirSync).toHaveBeenCalledWith("/some/path", { recursive: true }); }); - it('does nothing if directory does exist', async () => { + it("does nothing if directory does exist", async () => { vi.mocked(fs.existsSync).mockReturnValueOnce(true); - ensureDir('/some/path'); + ensureDir("/some/path"); expect(fs.mkdirSync).not.toHaveBeenCalled(); }); }); -describe('outputFile', () => { - it('writes the given data to the given file', async () => { - await outputFile('/some/path', 'some data'); - expect(fsPromises.writeFile).toHaveBeenCalledWith('/some/path', 'some data', { mode: 511 }); +describe("outputFile", () => { + it("writes the given data to the given file", async () => { + await outputFile("/some/path", "some data"); + expect(fsPromises.writeFile).toHaveBeenCalledWith("/some/path", "some data", { mode: 511 }); }); }); -describe('outputJSONFile', () => { - it('writes the given JSON data to the given file', async () => { - await outputJSONFile('/some/path', { data: 'some data ' }); +describe("outputJSONFile", () => { + it("writes the given JSON data to the given file", async () => { + await outputJSONFile("/some/path", { data: "some data " }); expect(fsPromises.writeFile).toHaveBeenCalledWith( - '/some/path', - JSON.stringify({ data: 'some data ' }), - { mode: 511 } + "/some/path", + JSON.stringify({ data: "some data " }), + { mode: 511 }, ); }); }); -describe('truncateFileName', () => { - it('does nothing if file name is within valid length', () => { - const filePath = 'this/is/a/valid.file.length'; +describe("truncateFileName", () => { + it("does nothing if file name is within valid length", () => { + const filePath = "this/is/a/valid.file.length"; const truncated = truncateFileName(filePath); expect(truncated).toEqual(filePath); - expect(truncated.split('/').at(-1)).toEqual('valid.file.length'); + expect(truncated.split("/").at(-1)).toEqual("valid.file.length"); }); - it('ignores length of path parts before the file name', () => { + it("ignores length of path parts before the file name", () => { const encoder = new TextEncoder(); const filePath = - '/a/bunch/of/paths/that/donot/affect-size/this-title-has-260-chars-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end.js'; + "/a/bunch/of/paths/that/donot/affect-size/this-title-has-260-chars-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end.js"; const filePathLength = encoder.encode(filePath).byteLength; expect(filePathLength).toBeGreaterThan(255); const truncated = truncateFileName(filePath); - expect(encoder.encode(truncated.split('/').at(-1)).byteLength).toEqual(255); + expect(encoder.encode(truncated.split("/").at(-1)).byteLength).toEqual(255); expect(truncated).toMatch( new RegExp( - '^/a/bunch/of/paths/that/donot/affect-size/this-title-.*ok-this-right-here-this-i[a-z0-9]{4}.js$' - ) + "^/a/bunch/of/paths/that/donot/affect-size/this-title-.*ok-this-right-here-this-i[a-z0-9]{4}.js$", + ), ); }); - it('truncates long file names without changing extension', () => { + it("truncates long file names without changing extension", () => { const encoder = new TextEncoder(); const fileName = - 'this-title-has-260-bytes-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end.js'; + "this-title-has-260-bytes-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end.js"; const fileNameLength = encoder.encode(fileName).byteLength; expect(fileNameLength).toBeGreaterThan(255); @@ -123,13 +125,13 @@ describe('truncateFileName', () => { const truncatedLength = encoder.encode(truncated).byteLength; expect(truncatedLength).toEqual(255); - expect(truncated).toMatch(new RegExp('^this-title-.*ok-this-right-here-this-i[a-z0-9]{4}.js$')); + expect(truncated).toMatch(new RegExp("^this-title-.*ok-this-right-here-this-i[a-z0-9]{4}.js$")); }); - it('correctly truncates file names with multi-byte characters', () => { + it("correctly truncates file names with multi-byte characters", () => { const encoder = new TextEncoder(); const fileName = - 'このタイトルは260byteあります-私が数えたので間違いないです-そしてそれはファイルシステムにとっては大きすぎます-ああだこうだ-ああだこうだ-ああだこうだ-ああだこうだ-これで終わりです.js'; + "このタイトルは260byteあります-私が数えたので間違いないです-そしてそれはファイルシステムにとっては大きすぎます-ああだこうだ-ああだこうだ-ああだこうだ-ああだこうだ-これで終わりです.js"; const fileNameLength = encoder.encode(fileName).byteLength; expect(fileNameLength).toBeGreaterThan(255); @@ -138,14 +140,14 @@ describe('truncateFileName', () => { expect(truncatedLength).toEqual(255); expect(truncated).toMatch( - new RegExp('^このタイトルは260byteあります-.*-ああだこうだ-これで終[a-z0-9]{4}.js$') + new RegExp("^このタイトルは260byteあります-.*-ああだこうだ-これで終[a-z0-9]{4}.js$"), ); }); - it('truncates long file names without changing multiple extensions', () => { + it("truncates long file names without changing multiple extensions", () => { const encoder = new TextEncoder(); const fileName = - 'this-title-has-260-bytes-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end.one.js'; + "this-title-has-260-bytes-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end.one.js"; const fileNameLength = encoder.encode(fileName).byteLength; expect(fileNameLength).toBeGreaterThan(255); @@ -153,13 +155,13 @@ describe('truncateFileName', () => { const truncatedLength = encoder.encode(truncated).byteLength; expect(truncatedLength).toEqual(255); - expect(truncated).toMatch(new RegExp('^this-title-.*ok-this-right-here-th[a-z0-9]{4}.one.js$')); + expect(truncated).toMatch(new RegExp("^this-title-.*ok-this-right-here-th[a-z0-9]{4}.one.js$")); }); - it('truncates long names without an extension', () => { + it("truncates long names without an extension", () => { const encoder = new TextEncoder(); const fileName = - 'this-title-has-260-bytes-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end'; + "this-title-has-260-bytes-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end"; const fileNameLength = encoder.encode(fileName).byteLength; expect(fileNameLength).toBeGreaterThan(255); @@ -167,13 +169,13 @@ describe('truncateFileName', () => { const truncatedLength = encoder.encode(truncated).byteLength; expect(truncatedLength).toEqual(255); - expect(truncated).toMatch(new RegExp('^this-title-.*ok-this-right-here-this-is-t[a-z0-9]{4}$')); + expect(truncated).toMatch(new RegExp("^this-title-.*ok-this-right-here-this-is-t[a-z0-9]{4}$")); }); - it('truncates long names to given size', () => { + it("truncates long names to given size", () => { const encoder = new TextEncoder(); const fileName = - 'this-title-has-260-bytes-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end'; + "this-title-has-260-bytes-exactly-i-know-because-i-counted-and-that-is-too-big-for-a-file-system-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-blah-b-ok-this-right-here-this-is-the-end"; const fileNameLength = encoder.encode(fileName).byteLength; expect(fileNameLength).toBeGreaterThan(255); @@ -181,35 +183,35 @@ describe('truncateFileName', () => { const truncatedLength = encoder.encode(truncated).byteLength; expect(truncatedLength).toEqual(100); - expect(truncated).toMatch(new RegExp('^this-title-.*-a-file-system-[a-z0-9]{4}$')); + expect(truncated).toMatch(new RegExp("^this-title-.*-a-file-system-[a-z0-9]{4}$")); }); - describe('removeLocalhostFromBaseUrl', () => { - it('should remove localhost from href but keeps the relative path', () => { - const href = 'http://localhost:3000/some/path/'; + describe("removeLocalhostFromBaseUrl", () => { + it("should remove localhost from href but keeps the relative path", () => { + const href = "http://localhost:3000/some/path/"; const result = removeLocalhostFromBaseUrl(href); - expect(result).toBe('/some/path/'); + expect(result).toBe("/some/path/"); }); - it('should remove localhost from href but keeps the search and hash paramters', () => { - const href = 'http://localhost:3000/some/path/?query=value#fragment'; + it("should remove localhost from href but keeps the search and hash paramters", () => { + const href = "http://localhost:3000/some/path/?query=value#fragment"; const result = removeLocalhostFromBaseUrl(href); - expect(result).toBe('/some/path/?query=value#fragment'); + expect(result).toBe("/some/path/?query=value#fragment"); }); - it('should remove localhost from href', () => { - const href = 'http://localhost:3000/'; + it("should remove localhost from href", () => { + const href = "http://localhost:3000/"; const result = removeLocalhostFromBaseUrl(href); - expect(result).toBe('/'); + expect(result).toBe("/"); }); - it('should return the exact href if it is already relative', () => { - const href = '/some/path/'; + it("should return the exact href if it is already relative", () => { + const href = "/some/path/"; const result = removeLocalhostFromBaseUrl(href); - expect(result).toBe('/some/path/'); + expect(result).toBe("/some/path/"); }); - it('should return the exact href if locahost is not the host name', () => { - const href = 'https://www.example.com/'; + it("should return the exact href if locahost is not the host name", () => { + const href = "https://www.example.com/"; const result = removeLocalhostFromBaseUrl(href); expect(result).toBe(href); }); diff --git a/packages/shared/src/utils/filePaths.ts b/packages/shared/src/utils/filePaths.ts index 7fcfd253..20c09580 100644 --- a/packages/shared/src/utils/filePaths.ts +++ b/packages/shared/src/utils/filePaths.ts @@ -1,7 +1,7 @@ -import { existsSync, mkdirSync } from 'node:fs'; -import { writeFile } from 'node:fs/promises'; -import { createHash } from 'node:crypto'; -import path from 'node:path'; +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import path from "node:path"; function rootDir() { return process.cwd(); @@ -14,14 +14,14 @@ function outputDirOverride() { export function archivesDir(defaultOutputDir: string) { const outputDir = outputDirOverride() || defaultOutputDir; - return path.resolve(rootDir(), outputDir, 'chromatic-archives'); + return path.resolve(rootDir(), outputDir, "chromatic-archives"); } export function checkArchivesDirExists(defaultOutputDir: string) { const dir = archivesDir(defaultOutputDir); if (!existsSync(dir)) { throw new Error( - `Chromatic archives directory cannot be found: ${dir}\n\nPlease make sure that you have run your E2E tests, or have set the CHROMATIC_ARCHIVE_LOCATION env var if the output directory for the tests is not in the standard location.` + `Chromatic archives directory cannot be found: ${dir}\n\nPlease make sure that you have run your E2E tests, or have set the CHROMATIC_ARCHIVE_LOCATION env var if the output directory for the tests is not in the standard location.`, ); } } @@ -44,7 +44,7 @@ export async function outputJSONFile(filePath: string, data: any) { // Generates a fixed length hash for the given `data` function hash(data: string) { // `outputLength` of 2 bytes is 4 chars - return createHash('shake256', { outputLength: 2 }).update(data).digest('hex'); + return createHash("shake256", { outputLength: 2 }).update(data).digest("hex"); } // 255 bytes is a good upper bound on file name size to work on most platforms @@ -57,12 +57,12 @@ export const MAX_FILE_NAME_BYTE_LENGTH = 255; // at the end that is truncated. export function truncateFileName( filePath: string, - maxByteLength: number = MAX_FILE_NAME_BYTE_LENGTH + maxByteLength: number = MAX_FILE_NAME_BYTE_LENGTH, ) { const encoder = new TextEncoder(); const decoder = new TextDecoder(); - const filePathParts = filePath.split('/'); + const filePathParts = filePath.split("/"); const fileName = filePathParts.pop(); const fileNameByteArray = encoder.encode(fileName); @@ -71,9 +71,9 @@ export function truncateFileName( } const hashedFileName = hash(fileName); - const [baseName, ...extensions] = fileName.split('.'); + const [baseName, ...extensions] = fileName.split("."); const baseNameByteArray = encoder.encode(baseName); - const ext = extensions.join('.'); + const ext = extensions.join("."); const extLength = ext.length === 0 ? 0 : encoder.encode(ext).byteLength + 1; // +1 for leading `.` if needed const lengthHashAndExt = encoder.encode(hashedFileName).byteLength + extLength; @@ -81,9 +81,9 @@ export function truncateFileName( const truncatedBaseName = decoder.decode(truncatedBaseNameByteArray); const truncatedFileName = [`${truncatedBaseName}${hashedFileName}`, ext] .filter(Boolean) - .join('.'); + .join("."); - return [...filePathParts, truncatedFileName].join('/'); + return [...filePathParts, truncatedFileName].join("/"); } /** @@ -94,7 +94,7 @@ export const removeLocalhostFromBaseUrl = (href: string) => { let baseUrl; try { baseUrl = new URL(href); - if (baseUrl.hostname === 'localhost') { + if (baseUrl.hostname === "localhost") { return baseUrl.pathname + baseUrl.search + baseUrl.hash; } return href; diff --git a/packages/shared/src/utils/nodes.ts b/packages/shared/src/utils/nodes.ts index ba78b0f7..60d29cbc 100644 --- a/packages/shared/src/utils/nodes.ts +++ b/packages/shared/src/utils/nodes.ts @@ -1,11 +1,12 @@ -import { NodeType, serializedNodeWithId } from '@rrweb/types'; -import type { SerializedIframeNode } from '../types'; +import { NodeType, serializedNodeWithId } from "@rrweb/types"; + +import type { SerializedIframeNode } from "../types"; export function isIframeSerializedNode(node: serializedNodeWithId): node is SerializedIframeNode { return ( node.type === NodeType.Element && - node.tagName === 'iframe' && - 'contentDocument' in node && + node.tagName === "iframe" && + "contentDocument" in node && !!node.contentDocument ); } @@ -15,5 +16,5 @@ export function isElement(node: Node | undefined): node is Element { } export function isIframeElement(node: Node): node is HTMLIFrameElement { - return isElement(node) && node.tagName.toLowerCase() === 'iframe'; + return isElement(node) && node.tagName.toLowerCase() === "iframe"; } diff --git a/packages/shared/src/utils/storybookPaths.ts b/packages/shared/src/utils/storybookPaths.ts index cc12666e..0df98820 100644 --- a/packages/shared/src/utils/storybookPaths.ts +++ b/packages/shared/src/utils/storybookPaths.ts @@ -1,5 +1,5 @@ -import path from 'node:path'; -import { createRequire } from 'node:module'; +import { createRequire } from "node:module"; +import path from "node:path"; /** * Directory of the `node_modules` that contains the `storybook` package this package depends on. @@ -13,10 +13,10 @@ import { createRequire } from 'node:module'; */ export function storybookParentNodeModulesDir(importMetaUrl: string): string | null { const require = createRequire(importMetaUrl); - const dir = path.dirname(path.dirname(require.resolve('storybook/package.json'))); + const dir = path.dirname(path.dirname(require.resolve("storybook/package.json"))); // If `storybook` resolves through a link to a source checkout (e.g. `pnpm link`, `portal:`), // its parent directory is not a node_modules dir and its siblings are not packages, so it // must not be offered to webpack as a module directory. - return path.basename(dir) === 'node_modules' ? dir : null; + return path.basename(dir) === "node_modules" ? dir : null; } diff --git a/packages/shared/src/write-archive/archive-file.test.ts b/packages/shared/src/write-archive/archive-file.test.ts index def7a888..185b547b 100644 --- a/packages/shared/src/write-archive/archive-file.test.ts +++ b/packages/shared/src/write-archive/archive-file.test.ts @@ -1,164 +1,165 @@ -import { describe, expect, it } from 'vitest'; -import { ArchiveFile } from './archive-file'; -import type { ArchiveResponse, UrlString } from '../resource-archiver'; +import { describe, expect, it } from "vitest"; + +import type { ArchiveResponse, UrlString } from "../resource-archiver"; +import { ArchiveFile } from "./archive-file"; const imgPng = - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; const response = { statusCode: 200, - statusText: 'ok', - body: Buffer.from(imgPng, 'base64'), - contentType: 'image/png', + statusText: "ok", + body: Buffer.from(imgPng, "base64"), + contentType: "image/png", }; -describe('ArchiveFile', () => { +describe("ArchiveFile", () => { const createArchiveFile = (fileUrl: UrlString, archiveResponse: ArchiveResponse = response) => { - return new ArchiveFile(fileUrl, archiveResponse, 'http://localhost:333'); + return new ArchiveFile(fileUrl, archiveResponse, "http://localhost:333"); }; - describe('toFileSystemPath', () => { - it('has no effect on valid paths', () => { - const archiveFile = createArchiveFile('http://localhost:333/some/directory/hi.png'); + describe("toFileSystemPath", () => { + it("has no effect on valid paths", () => { + const archiveFile = createArchiveFile("http://localhost:333/some/directory/hi.png"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toEqual('/some/directory/hi.png'); + expect(filePath).toEqual("/some/directory/hi.png"); }); - it('ensures path is not a directory', () => { - const archiveFile = createArchiveFile('http://localhost:333/some/directory/'); + it("ensures path is not a directory", () => { + const archiveFile = createArchiveFile("http://localhost:333/some/directory/"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toEqual('/some/directory/index.html'); + expect(filePath).toEqual("/some/directory/index.html"); }); - it('appends encoded query string to file name', () => { + it("appends encoded query string to file name", () => { const archiveFile = createArchiveFile( - 'http://localhost:333/some/directory/img?src=https://someotherdomain.com/image.jpg' + "http://localhost:333/some/directory/img?src=https://someotherdomain.com/image.jpg", ); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toMatch(new RegExp('/some/directory/img-[a-z0-9]+.png')); + expect(filePath).toMatch(new RegExp("/some/directory/img-[a-z0-9]+.png")); }); - it('keeps a valid extension when an extensioned asset has a query string', () => { + it("keeps a valid extension when an extensioned asset has a query string", () => { const archiveFile = createArchiveFile( - 'http://localhost:333/assets/logo-light.somehash.svg?dpl=abc123', + "http://localhost:333/assets/logo-light.somehash.svg?dpl=abc123", { statusCode: 200, - statusText: 'ok', + statusText: "ok", body: Buffer.from(''), - contentType: 'image/svg+xml', - } + contentType: "image/svg+xml", + }, ); expect(archiveFile.toFileSystemPath()).toMatchInlineSnapshot( - `"/assets/logo-light.somehash-53ba8980be568e19a9248ff35298ebb4.svg"` + `"/assets/logo-light.somehash-53ba8980be568e19a9248ff35298ebb4.svg"`, ); }); - it('truncates long file names and path parts', () => { - const archiveFile = createArchiveFile('http://localhost:333/some/directory/ok.jpg'); + it("truncates long file names and path parts", () => { + const archiveFile = createArchiveFile("http://localhost:333/some/directory/ok.jpg"); archiveFile.shortenedFileNameLength = 5; const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toEqual('/some/direc/ok.jp'); + expect(filePath).toEqual("/some/direc/ok.jp"); }); - it('adds a file extension based on content type when there is not one already', () => { - const archiveFile = createArchiveFile('http://localhost:333/some/directory/ok'); + it("adds a file extension based on content type when there is not one already", () => { + const archiveFile = createArchiveFile("http://localhost:333/some/directory/ok"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toEqual('/some/directory/ok.png'); + expect(filePath).toEqual("/some/directory/ok.png"); }); - it('adds a default tmp file extension when response has no content type', () => { + it("adds a default tmp file extension when response has no content type", () => { const noContentType = { ...response }; delete noContentType.contentType; const archiveFile = createArchiveFile( - 'http://localhost:333/some/directory/ok', - noContentType + "http://localhost:333/some/directory/ok", + noContentType, ); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toEqual('/some/directory/ok.tmp'); + expect(filePath).toEqual("/some/directory/ok.tmp"); }); - it('prepends domain name (if archiving additional domains)', () => { - const archiveFile = createArchiveFile('http://subdomain.some-other-host/some-path/me.png'); + it("prepends domain name (if archiving additional domains)", () => { + const archiveFile = createArchiveFile("http://subdomain.some-other-host/some-path/me.png"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toEqual('/subdomain.some-other-host/some-path/me.png'); + expect(filePath).toEqual("/subdomain.some-other-host/some-path/me.png"); }); - it('prepends domain name if port is all that differs', () => { - const archiveFile = createArchiveFile('http://localhost:9999/some/directory/hi.png'); + it("prepends domain name if port is all that differs", () => { + const archiveFile = createArchiveFile("http://localhost:9999/some/directory/hi.png"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toEqual('/localhost3A9999/some/directory/hi.png'); + expect(filePath).toEqual("/localhost3A9999/some/directory/hi.png"); }); - it('appends encoded string to reserved SB files', () => { - const archiveFile = createArchiveFile('http://localhost:333/index.json'); + it("appends encoded string to reserved SB files", () => { + const archiveFile = createArchiveFile("http://localhost:333/index.json"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toMatch(new RegExp('/index-[a-z0-9]+.json')); + expect(filePath).toMatch(new RegExp("/index-[a-z0-9]+.json")); }); - it('appends encoded string to reserved SB files: main.iframe', () => { - const archiveFile = createArchiveFile('http://localhost:333/main.iframe.bundle.js'); + it("appends encoded string to reserved SB files: main.iframe", () => { + const archiveFile = createArchiveFile("http://localhost:333/main.iframe.bundle.js"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toMatch(new RegExp('/main.iframe.bundle-[a-z0-9]+.js')); + expect(filePath).toMatch(new RegExp("/main.iframe.bundle-[a-z0-9]+.js")); }); - it('appends encoded string to reserved SB files: runtime~main.iframe', () => { - const archiveFile = createArchiveFile('http://localhost:333/runtime~main.iframe.bundle.js'); + it("appends encoded string to reserved SB files: runtime~main.iframe", () => { + const archiveFile = createArchiveFile("http://localhost:333/runtime~main.iframe.bundle.js"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toMatch(new RegExp('/runtime~main.iframe.bundle-[a-z0-9]+.js')); + expect(filePath).toMatch(new RegExp("/runtime~main.iframe.bundle-[a-z0-9]+.js")); }); - it('appends encoded string to reserved SB files: sb-preview/runtime.js', () => { - const archiveFile = createArchiveFile('http://localhost:333/sb-preview/runtime.js'); + it("appends encoded string to reserved SB files: sb-preview/runtime.js", () => { + const archiveFile = createArchiveFile("http://localhost:333/sb-preview/runtime.js"); const filePath = archiveFile.toFileSystemPath(); - expect(filePath).toMatch(new RegExp('/sb-preview/runtime-[a-z0-9]+.js')); + expect(filePath).toMatch(new RegExp("/sb-preview/runtime-[a-z0-9]+.js")); }); }); - describe('originalSrc', () => { - it('retains the original source from the asset URL', () => { + describe("originalSrc", () => { + it("retains the original source from the asset URL", () => { const archiveFile = createArchiveFile( - 'http://localhost:333/some/directory/ok?src=some-other-url' + "http://localhost:333/some/directory/ok?src=some-other-url", ); archiveFile.toFileSystemPath(); const originalSrc = archiveFile.originalSrc(); - expect(originalSrc).toEqual('http://localhost:333/some/directory/ok?src=some-other-url'); + expect(originalSrc).toEqual("http://localhost:333/some/directory/ok?src=some-other-url"); }); - it('retains the domain from the asset URL if cross-domain', () => { - const archiveFile = createArchiveFile('http://subdomain.some-other-host/some-path/me.png'); + it("retains the domain from the asset URL if cross-domain", () => { + const archiveFile = createArchiveFile("http://subdomain.some-other-host/some-path/me.png"); archiveFile.toFileSystemPath(); const originalSrc = archiveFile.originalSrc(); - expect(originalSrc).toEqual('http://subdomain.some-other-host/some-path/me.png'); + expect(originalSrc).toEqual("http://subdomain.some-other-host/some-path/me.png"); }); }); }); diff --git a/packages/shared/src/write-archive/archive-file.ts b/packages/shared/src/write-archive/archive-file.ts index 49f0f4af..0adc3eb5 100644 --- a/packages/shared/src/write-archive/archive-file.ts +++ b/packages/shared/src/write-archive/archive-file.ts @@ -1,20 +1,22 @@ -import path from 'node:path'; -import { createHash } from 'node:crypto'; -import mime from 'mime'; -import { logger } from '../utils/logger'; -import type { ArchiveResponse, UrlString } from '../resource-archiver'; +import { createHash } from "node:crypto"; +import path from "node:path"; + +import mime from "mime"; + +import type { ArchiveResponse, UrlString } from "../resource-archiver"; +import { logger } from "../utils/logger"; const RESERVED_PATHNAMES: string[] = [ // This is a list of filenames that are reserved for Storybook as a dev target. // If we encounter these filenames, we will rename them to avoid collisions. // If we do not rename them, the build-archive-storybook will not be able to serve the correct archives // and the tests will fail. - '/index.json', - '/iframe.html', - '/index.html', - '/main.iframe.bundle.js', - '/runtime~main.iframe.bundle.js', - '/sb-preview/runtime.js', + "/index.json", + "/iframe.html", + "/index.html", + "/main.iframe.bundle.js", + "/runtime~main.iframe.bundle.js", + "/sb-preview/runtime.js", ]; /** @@ -67,7 +69,7 @@ export class ArchiveFile { } private ensureNonDirectory(pathname: string) { - return pathname.endsWith('/') ? `${pathname}index.html` : pathname; + return pathname.endsWith("/") ? `${pathname}index.html` : pathname; } private encodeQueryString(pathname: string) { @@ -86,7 +88,7 @@ export class ArchiveFile { private truncateFileName(pathname: string) { // Split this path to get all of the pieces - const pathPieces = pathname.split('/'); + const pathPieces = pathname.split("/"); const rebuiltPieces: Array = []; // Shorten each individual piece @@ -101,21 +103,21 @@ export class ArchiveFile { }); // Re-join the pieces - return rebuiltPieces.join('/'); + return rebuiltPieces.join("/"); } private removeSpecialChars(pathname: string) { // The storybook server seems to have a problem with percents in file names - return pathname.replace(/[%:@]/g, ''); + return pathname.replace(/[%:@]/g, ""); } private addExtension(pathname: string) { - if ('error' in this.response) return pathname; + if ("error" in this.response) return pathname; // Add an extension if needed let nameWithExtension = pathname; if (!path.extname(nameWithExtension)) { - const fileExtension = mime.getExtension(this.response.contentType) || 'tmp'; + const fileExtension = mime.getExtension(this.response.contentType) || "tmp"; nameWithExtension = `${pathname}.${fileExtension}`; } @@ -134,7 +136,7 @@ export class ArchiveFile { } private hash(name: string) { - return createHash('md5').update(name).digest('hex'); + return createHash("md5").update(name).digest("hex"); } private renameReservedPathnames(pathname: string) { diff --git a/packages/shared/src/write-archive/dom-snapshot.test.ts b/packages/shared/src/write-archive/dom-snapshot.test.ts index c7221b17..eb70dcd0 100644 --- a/packages/shared/src/write-archive/dom-snapshot.test.ts +++ b/packages/shared/src/write-archive/dom-snapshot.test.ts @@ -1,11 +1,12 @@ -import { describe, expect, it } from 'vitest'; -import { serializedNodeWithId } from '@rrweb/types'; -import { DOMSnapshot } from './dom-snapshot'; +import { serializedNodeWithId } from "@rrweb/types"; +import { describe, expect, it } from "vitest"; -const relativeUrl = '/images/image.png'; -const externalUrl = 'https://chromatic.com/img'; -const queryUrl = '/images/img?src=https://chromatic.com/img'; -const queryUrlTransformed = '/images/somehash.png'; +import { DOMSnapshot } from "./dom-snapshot"; + +const relativeUrl = "/images/image.png"; +const externalUrl = "https://chromatic.com/img"; +const queryUrl = "/images/img?src=https://chromatic.com/img"; +const queryUrlTransformed = "/images/somehash.png"; function createSnapshot(url1: string, url2: string, url3: string) { return `{"type":0,"childNodes":[{"type":1,"name":"html","publicId":"","systemId":"","id":2},{"type":2,"tagName":"html","attributes":{},"childNodes":[{"type":2,"tagName":"head","attributes":{},"childNodes":[{"type":3,"textContent":" ","id":5},{"type":2,"tagName":"link","attributes":{"rel":"stylesheet","href":"/styles-test.css"},"childNodes":[],"id":6},{"type":3,"textContent":" ","id":7},{"type":2,"tagName":"style","attributes":{},"childNodes":[{"type":3,"textContent":".test1 { background-image: url(\\"${url1}\\"); }.test2 { background-image: url(\\"${url2}\\"); }.test2 { background-image: url(\\"${url3}\\"); }","isStyle":true,"id":9}],"id":8},{"type":3,"textContent":" ","id":10}],"id":4},{"type":3,"textContent":" ","id":11},{"type":2,"tagName":"body","attributes":{},"childNodes":[{"type":3,"textContent":" ","id":13},{"type":2,"tagName":"div","attributes":{"class":"image-container flex flex-wrap"},"childNodes":[{"type":3,"textContent":" ","id":15},{"type":2,"tagName":"img","attributes":{"src":"${url1}"},"childNodes":[],"id":16},{"type":3,"textContent":" ","id":17},{"type":2,"tagName":"img","attributes":{"src":"${url2}"},"childNodes":[],"id":18},{"type":3,"textContent":" ","id":19},{"type":2,"tagName":"img","attributes":{"src":"${url3}"},"childNodes":[],"id":20},{"type":3,"textContent":" ","id":21},{"type":2,"tagName":"div","attributes":{"style":"background: url('${url1}'); no-repeat center;"},"childNodes":[],"id":22},{"type":3,"textContent":" ","id":23},{"type":2,"tagName":"div","attributes":{"style":"background: url('${url2}'); no-repeat center;"},"childNodes":[],"id":24},{"type":3,"textContent":" ","id":25},{"type":2,"tagName":"div","attributes":{"style":"background: url('${url3}'); no-repeat center;"},"childNodes":[],"id":26},{"type":3,"textContent":" ","id":27}],"id":14},{"type":3,"textContent":" ","id":28}],"id":12}],"id":3}],"id":1}`; @@ -14,7 +15,7 @@ function createSnapshot(url1: string, url2: string, url3: string) { function createBaseTagSnapshot(url: string) { return JSON.stringify({ type: 2, - tagName: 'base', + tagName: "base", attributes: { href: url, }, @@ -34,10 +35,10 @@ function createImgSrcsetSnapshot({ }) { return JSON.stringify({ type: 2, - tagName: 'img', + tagName: "img", attributes: { srcset: `${smallUrl} 384w, ${largeUrl} 1920w`, - sizes: '(min-width: 768px) 768px, 192px', + sizes: "(min-width: 768px) 768px, 192px", src: backupUrl, }, childNodes: [], @@ -54,12 +55,12 @@ function createPictureSrcsetSnapshotSingleSource({ }) { return JSON.stringify({ type: 2, - tagName: 'picture', + tagName: "picture", attributes: {}, childNodes: [ { type: 2, - tagName: 'source', + tagName: "source", attributes: { srcset: matchingUrl, }, @@ -68,7 +69,7 @@ function createPictureSrcsetSnapshotSingleSource({ }, { type: 2, - tagName: 'img', + tagName: "img", attributes: { src: backupUrl, }, @@ -89,12 +90,12 @@ function createPictureSrcsetSnapshotSingleSourceImageHasAttributes({ }) { return JSON.stringify({ type: 2, - tagName: 'picture', + tagName: "picture", attributes: {}, childNodes: [ { type: 2, - tagName: 'source', + tagName: "source", attributes: { srcset: matchingUrl, }, @@ -103,9 +104,9 @@ function createPictureSrcsetSnapshotSingleSourceImageHasAttributes({ }, { type: 2, - tagName: 'img', + tagName: "img", attributes: { - class: 'do-not-remove-me', + class: "do-not-remove-me", src: backupUrl, }, childNodes: [], @@ -127,28 +128,28 @@ function createPictureSrcsetSnapshotSingleSourceMultipleSrcsets({ }) { return JSON.stringify({ type: 2, - tagName: 'picture', + tagName: "picture", attributes: {}, childNodes: [ { type: 2, - tagName: 'source', + tagName: "source", attributes: { expectedMappedSnapshot, // source typically has multiple sizes of .webp (or other modern format) srcset: `${wrongUrl} 2000w, ${matchingUrl} 900w`, - sizes: '(min-width: 768px) 768px, 192px', + sizes: "(min-width: 768px) 768px, 192px", }, childNodes: [], id: 63, }, { type: 2, - tagName: 'img', + tagName: "img", attributes: { // image typically has multiple sizes of .jpg (or other highly-compatible format) srcset: `${backupUrl} 384w, ${backupUrl} 1920w`, - sizes: '(min-width: 768px) 768px, 192px', + sizes: "(min-width: 768px) 768px, 192px", // src is the final fallback for browsers that don't support srcset src: backupUrl, }, @@ -171,12 +172,12 @@ function createPictureSrcsetSnapshotMultipleSources({ }) { return JSON.stringify({ type: 2, - tagName: 'picture', + tagName: "picture", attributes: {}, childNodes: [ { type: 2, - tagName: 'source', + tagName: "source", attributes: { srcset: wrongUrl, }, @@ -185,7 +186,7 @@ function createPictureSrcsetSnapshotMultipleSources({ }, { type: 2, - tagName: 'source', + tagName: "source", attributes: { srcset: matchingUrl, }, @@ -194,7 +195,7 @@ function createPictureSrcsetSnapshotMultipleSources({ }, { type: 2, - tagName: 'img', + tagName: "img", attributes: { src: backupUrl, }, @@ -215,12 +216,12 @@ function createPictureSrcsetNoUrlMatches({ }) { return JSON.stringify({ type: 2, - tagName: 'picture', + tagName: "picture", attributes: {}, childNodes: [ { type: 2, - tagName: 'source', + tagName: "source", attributes: { srcset: wrongUrl, }, @@ -229,7 +230,7 @@ function createPictureSrcsetNoUrlMatches({ }, { type: 2, - tagName: 'img', + tagName: "img", attributes: { src: alsoWrongUrl, }, @@ -243,7 +244,7 @@ function createPictureSrcsetNoUrlMatches({ const imageTag = { type: 2, - tagName: 'img', + tagName: "img", attributes: { src: queryUrlTransformed, }, @@ -253,7 +254,7 @@ const imageTag = { const pictureWithJustImageTag = { type: 2, - tagName: 'picture', + tagName: "picture", attributes: {}, childNodes: [imageTag], id: 62, @@ -265,9 +266,9 @@ const expectedMappedSnapshot = createSnapshot(relativeUrl, externalUrl, queryUrl const sourceMap = new Map(); sourceMap.set(queryUrl, queryUrlTransformed); -describe('DOMSnapshot', () => { - describe('mapAssetPaths', () => { - it('maps asset paths in src attrs, style attrs, and external style sheets, and inline style elements', async () => { +describe("DOMSnapshot", () => { + describe("mapAssetPaths", () => { + it("maps asset paths in src attrs, style attrs, and external style sheets, and inline style elements", async () => { const domSnapshot = new DOMSnapshot({ snapshot, pseudoClassIds: {} }); const mappedSnapshot = await domSnapshot.mapAssetPaths(sourceMap); @@ -275,7 +276,7 @@ describe('DOMSnapshot', () => { expect(mappedSnapshot).toEqual(`{"snapshot":${expectedMappedSnapshot},"pseudoClassIds":{}}`); }); - it('does not change paths that are not in the source map', async () => { + it("does not change paths that are not in the source map", async () => { const domSnapshot = new DOMSnapshot({ snapshot, pseudoClassIds: {} }); const mappedSnapshot = await domSnapshot.mapAssetPaths(new Map()); @@ -283,7 +284,7 @@ describe('DOMSnapshot', () => { expect(mappedSnapshot).toEqual(`{"snapshot":${snapshot},"pseudoClassIds":{}}`); }); - it('maps img srcsets', async () => { + it("maps img srcsets", async () => { const domSnapshot = new DOMSnapshot({ snapshot: createImgSrcsetSnapshot({ backupUrl: relativeUrl, @@ -298,7 +299,7 @@ describe('DOMSnapshot', () => { expect(JSON.parse(mappedSnapshot)).toEqual({ snapshot: { type: 2, - tagName: 'img', + tagName: "img", attributes: { src: `${queryUrlTransformed}`, }, @@ -309,7 +310,7 @@ describe('DOMSnapshot', () => { }); }); - it('does not change img srcsets when no mapped asset found in source map', async () => { + it("does not change img srcsets when no mapped asset found in source map", async () => { const originalSnapshot = createImgSrcsetSnapshot({ backupUrl: relativeUrl, smallUrl: externalUrl, @@ -322,7 +323,7 @@ describe('DOMSnapshot', () => { expect(mappedSnapshot).toEqual(`{"snapshot":${originalSnapshot},"pseudoClassIds":{}}`); }); - it('maps picture srcsets, single ', async () => { + it("maps picture srcsets, single ", async () => { const domSnapshot = new DOMSnapshot({ snapshot: createPictureSrcsetSnapshotSingleSource({ backupUrl: relativeUrl, @@ -336,7 +337,7 @@ describe('DOMSnapshot', () => { expect(JSON.parse(mappedSnapshot).snapshot).toEqual(pictureWithJustImageTag); }); - it('maps picture srcsets, multiple s', async () => { + it("maps picture srcsets, multiple s", async () => { const domSnapshot = new DOMSnapshot({ snapshot: createPictureSrcsetSnapshotMultipleSources({ backupUrl: relativeUrl, @@ -351,7 +352,7 @@ describe('DOMSnapshot', () => { expect(JSON.parse(mappedSnapshot).snapshot).toEqual(pictureWithJustImageTag); }); - it('maps picture srcsets, single with multiple srcset values', async () => { + it("maps picture srcsets, single with multiple srcset values", async () => { const domSnapshot = new DOMSnapshot({ snapshot: createPictureSrcsetSnapshotSingleSourceMultipleSrcsets({ backupUrl: relativeUrl, @@ -366,10 +367,10 @@ describe('DOMSnapshot', () => { expect(JSON.parse(mappedSnapshot).snapshot).toEqual(pictureWithJustImageTag); }); - it('maps picture srcsets, and children left untouched if there is no URL match', async () => { + it("maps picture srcsets, and children left untouched if there is no URL match", async () => { const originalSnapshot = createPictureSrcsetNoUrlMatches({ - wrongUrl: '/totally-bogus-url.png', - alsoWrongUrl: 'https://another-totally-bogus.com/url.png', + wrongUrl: "/totally-bogus-url.png", + alsoWrongUrl: "https://another-totally-bogus.com/url.png", }); const domSnapshot = new DOMSnapshot({ snapshot: originalSnapshot, pseudoClassIds: {} }); @@ -380,7 +381,7 @@ describe('DOMSnapshot', () => { // important that we only blow away what we need to; since contents are styled by their tag, // we don't want to get rid of any existing attributes (like class for example) - it('maps picture srcsets, preserves existing attributes while stripping srcset and sizes', async () => { + it("maps picture srcsets, preserves existing attributes while stripping srcset and sizes", async () => { const domSnapshot = new DOMSnapshot({ snapshot: createPictureSrcsetSnapshotSingleSourceImageHasAttributes({ backupUrl: relativeUrl, @@ -398,37 +399,37 @@ describe('DOMSnapshot', () => { ...imageTag, attributes: { ...imageTag.attributes, - class: 'do-not-remove-me', + class: "do-not-remove-me", }, }, ], }); }); - it('does change base tag href when there is a localhost', async () => { - const originalSnapshot = createBaseTagSnapshot('http://localhost:3000/'); + it("does change base tag href when there is a localhost", async () => { + const originalSnapshot = createBaseTagSnapshot("http://localhost:3000/"); const domSnapshot = new DOMSnapshot({ snapshot: originalSnapshot, pseudoClassIds: {} }); const mappedSnapshot = await domSnapshot.mapAssetPaths(new Map()); expect(mappedSnapshot).toEqual( - `{"snapshot":${createBaseTagSnapshot('/')},"pseudoClassIds":{}}` + `{"snapshot":${createBaseTagSnapshot("/")},"pseudoClassIds":{}}`, ); }); - it('does not change base tag href when not localhost', async () => { - const originalSnapshot = createBaseTagSnapshot('https://example.com/app/'); + it("does not change base tag href when not localhost", async () => { + const originalSnapshot = createBaseTagSnapshot("https://example.com/app/"); const domSnapshot = new DOMSnapshot({ snapshot: originalSnapshot, pseudoClassIds: {} }); const mappedSnapshot = await domSnapshot.mapAssetPaths(new Map()); expect(mappedSnapshot).toEqual(`{"snapshot":${originalSnapshot},"pseudoClassIds":{}}`); }); - it('does not change base tag href when already relative', async () => { - const originalSnapshot = createBaseTagSnapshot('/app/'); + it("does not change base tag href when already relative", async () => { + const originalSnapshot = createBaseTagSnapshot("/app/"); const domSnapshot = new DOMSnapshot({ snapshot: originalSnapshot, pseudoClassIds: {} }); const mappedSnapshot = await domSnapshot.mapAssetPaths(new Map()); expect(mappedSnapshot).toEqual(`{"snapshot":${originalSnapshot},"pseudoClassIds":{}}`); }); - it('maps pseudoClassIds', async () => { + it("maps pseudoClassIds", async () => { const domSnapshot = new DOMSnapshot({ snapshot, pseudoClassIds: { hover: [2, 3, 4], focus: [5, 6, 7], active: [8, 9, 10] }, @@ -437,7 +438,7 @@ describe('DOMSnapshot', () => { const mappedSnapshot = await domSnapshot.mapAssetPaths(sourceMap); expect(mappedSnapshot).toEqual( - `{"snapshot":${expectedMappedSnapshot},"pseudoClassIds":{"hover":[2,3,4],"focus":[5,6,7],"active":[8,9,10]}}` + `{"snapshot":${expectedMappedSnapshot},"pseudoClassIds":{"hover":[2,3,4],"focus":[5,6,7],"active":[8,9,10]}}`, ); }); }); diff --git a/packages/shared/src/write-archive/dom-snapshot.ts b/packages/shared/src/write-archive/dom-snapshot.ts index 8223c44c..9b598f97 100644 --- a/packages/shared/src/write-archive/dom-snapshot.ts +++ b/packages/shared/src/write-archive/dom-snapshot.ts @@ -1,9 +1,10 @@ -import type { serializedElementNodeWithId, serializedNodeWithId } from '@rrweb/types'; -import { NodeType } from '@rrweb/types'; -import srcset from 'srcset'; -import { removeLocalhostFromBaseUrl } from '../utils/filePaths'; -import { isIframeSerializedNode } from '../utils/nodes'; -import type { DOMSnapshots, SavedSnapshot } from '../types'; +import type { serializedElementNodeWithId, serializedNodeWithId } from "@rrweb/types"; +import { NodeType } from "@rrweb/types"; +import srcset from "srcset"; + +import type { DOMSnapshots, SavedSnapshot } from "../types"; +import { removeLocalhostFromBaseUrl } from "../utils/filePaths"; +import { isIframeSerializedNode } from "../utils/nodes"; // Matches `url(...)` function in CSS text, excluding data URLs const CSS_URL_REGEX = /url\((?!['"]?(?:data):)['"]?([^'")]*)['"]?\)/gi; @@ -13,17 +14,17 @@ const CSS_URL_REGEX = /url\((?!['"]?(?:data):)['"]?([^'")]*)['"]?\)/gi; */ export class DOMSnapshot { snapshot: serializedNodeWithId; - pseudoClassIds: DOMSnapshots[string]['pseudoClassIds']; + pseudoClassIds: DOMSnapshots[string]["pseudoClassIds"]; constructor({ snapshot, pseudoClassIds, }: { - snapshot: DOMSnapshots[string]['snapshot'] | string; - pseudoClassIds: DOMSnapshots[string]['pseudoClassIds']; + snapshot: DOMSnapshots[string]["snapshot"] | string; + pseudoClassIds: DOMSnapshots[string]["pseudoClassIds"]; }) { if (Buffer.isBuffer(snapshot)) { - const bufferAsString = snapshot.toString('utf-8'); + const bufferAsString = snapshot.toString("utf-8"); this.snapshot = JSON.parse(bufferAsString); } else { this.snapshot = JSON.parse(snapshot); @@ -45,11 +46,11 @@ export class DOMSnapshot { node = this.mapNodeAttributes(node, sourceMap); node = this.mapTextElement(node, sourceMap); - if ('childNodes' in node) { + if ("childNodes" in node) { node.childNodes = await Promise.all( node.childNodes.map(async (childNode) => { return this.mapNode(childNode, sourceMap); - }) + }), ); } @@ -83,17 +84,17 @@ export class DOMSnapshot { node.attributes._cssText = mappedCssText; } - if (['link', 'use'].includes(node.tagName) && node.attributes.href) { + if (["link", "use"].includes(node.tagName) && node.attributes.href) { const hrefVal = node.attributes.href as string; // SVGs and other href values can have an anchor which will not be included // in the source map lookup. We'll remove it to do the lookup and add it back // onto the found value. - const hrefValAndAnchor = hrefVal.split('#'); + const hrefValAndAnchor = hrefVal.split("#"); const hrefValWithoutAnchor = hrefValAndAnchor[0]; if (sourceMap.has(hrefValWithoutAnchor)) { hrefValAndAnchor[0] = sourceMap.get(hrefValWithoutAnchor); - node.attributes.href = hrefValAndAnchor.join('#'); + node.attributes.href = hrefValAndAnchor.join("#"); } } @@ -105,7 +106,7 @@ export class DOMSnapshot { // the one asset that the browser decided to load when this was rendered. We don't want // the browser to try to load one of the others when this snapshot is rendered in Chromatic // because we won't have archived them. - if (node.tagName === 'img' && node.attributes.srcset) { + if (node.tagName === "img" && node.attributes.srcset) { const srcsetValue = node.attributes.srcset as string; const currentSrc = this.mapSrcsetUrls(srcsetValue, sourceMap); if (currentSrc) { @@ -114,12 +115,12 @@ export class DOMSnapshot { } } - if (node.tagName === 'picture') { + if (node.tagName === "picture") { this.mapPictureElement(node, sourceMap); } // If the element is a base tag, we remove the localhost from the href - if (node.tagName === 'base' && node.attributes.href) { + if (node.tagName === "base" && node.attributes.href) { node.attributes.href = removeLocalhostFromBaseUrl(node.attributes.href as string); } } @@ -129,11 +130,11 @@ export class DOMSnapshot { private mapPictureElement(node: serializedElementNodeWithId, sourceMap: Map) { const allSourceUrls: string[] = node.childNodes - .filter(this.isSourceElement) + .filter((element) => this.isSourceElement(element)) .map((childNode: serializedElementNodeWithId) => { // there can be multiple values in a single srcset, extract all of them const sourceSetValues = srcset.parse( - (childNode.attributes?.srcset as string | undefined) ?? '' + (childNode.attributes?.srcset as string | undefined) ?? "", ); return sourceSetValues.map((srcSetValue) => srcSetValue.url); }) @@ -153,7 +154,7 @@ export class DOMSnapshot { // replace the tag's `src` with this asset-mapped URL const imageElement = node.childNodes.find( - (childNode) => childNode.type === NodeType.Element && childNode.tagName === 'img' + (childNode) => childNode.type === NodeType.Element && childNode.tagName === "img", ) as serializedElementNodeWithId; if (imageElement && imageElement.attributes) { // we're assuming that whatever was archived is an image URL @@ -166,7 +167,7 @@ export class DOMSnapshot { } private isSourceElement(childNode: serializedNodeWithId) { - return childNode.type === NodeType.Element && childNode.tagName === 'source'; + return childNode.type === NodeType.Element && childNode.tagName === "source"; } private mapTextElement(node: serializedNodeWithId, sourceMap: Map) { @@ -202,7 +203,7 @@ export class DOMSnapshot { return currentSrc; } - private removeResponsiveImageAttributes(attributes: serializedElementNodeWithId['attributes']) { + private removeResponsiveImageAttributes(attributes: serializedElementNodeWithId["attributes"]) { // Remove srcset attributes since we'll only have the one that // loaded on render archived delete attributes.srcset; diff --git a/packages/shared/src/write-archive/index.test.ts b/packages/shared/src/write-archive/index.test.ts index 8c938fd8..23a64f73 100644 --- a/packages/shared/src/write-archive/index.test.ts +++ b/packages/shared/src/write-archive/index.test.ts @@ -1,11 +1,13 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { resolve } from 'path'; -import { NodeType } from '@rrweb/types'; -import * as filePaths from '../utils/filePaths'; -import { writeTestResult } from '.'; -import { uniqueId } from './stories-files'; +import { resolve } from "node:path"; -vi.mock(import('../utils/filePaths'), async (importOriginal) => ({ +import { NodeType } from "@rrweb/types"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { writeTestResult } from "."; +import * as filePaths from "../utils/filePaths"; +import { uniqueId } from "./stories-files"; + +vi.mock(import("../utils/filePaths"), async (importOriginal) => ({ ...(await importOriginal()), ensureDir: vi.fn(), outputFile: vi.fn(), @@ -17,13 +19,13 @@ const snapshotJson = { { type: NodeType.Element, attributes: { - src: 'http://localhost:3000/home/', + src: "http://localhost:3000/home/", }, }, { type: NodeType.Element, attributes: { - src: 'http://localhost:3000/img?src=some-path', + src: "http://localhost:3000/img?src=some-path", }, }, ], @@ -34,14 +36,14 @@ afterEach(() => { uniqueId.value = 1; }); -describe('writeTestResult', () => { - it('successfully generates test results', async () => { +describe("writeTestResult", () => { + it("successfully generates test results", async () => { await writeTestResult( // the default output directory in playwright { - titlePath: ['file.spec.ts', 'Test Story'], - outputDir: resolve('test-results'), - pageUrl: 'http://localhost:3000/', + titlePath: ["file.spec.ts", "Test Story"], + outputDir: resolve("test-results"), + pageUrl: "http://localhost:3000/", }, { home: { @@ -49,61 +51,61 @@ describe('writeTestResult', () => { viewport: { height: 800, width: 800 }, }, }, - { 'http://localhost:3000/home': { statusCode: 200, body: Buffer.from('Chromatic') } }, + { "http://localhost:3000/home": { statusCode: 200, body: Buffer.from("Chromatic") } }, { diffThreshold: 5, pauseAnimationAtEnd: true, - } + }, ); expect(filePaths.ensureDir).toHaveBeenCalledTimes(1); expect(filePaths.outputFile).toHaveBeenCalledTimes(2); expect(filePaths.outputJSONFile).toHaveBeenCalledTimes(1); expect(filePaths.outputJSONFile).toHaveBeenCalledWith( - resolve('./test-results/chromatic-archives/file-test-story.stories.json'), + resolve("./test-results/chromatic-archives/file-test-story.stories.json"), { stories: [ { - name: 'home', - globals: { viewport: 'w800h800' }, + name: "home", + globals: { viewport: "w800h800" }, parameters: { - __id: 'file-test-story--home', + __id: "file-test-story--home", chromatic: { diffThreshold: 5, pauseAnimationAtEnd: true, - modes: { w800h800: { viewport: 'w800h800' } }, + modes: { w800h800: { viewport: "w800h800" } }, }, - server: { id: 'file-test-story-home' }, + server: { id: "file-test-story-home" }, viewport: { - defaultViewport: 'w800h800', + defaultViewport: "w800h800", options: { w800h800: { - name: 'w800h800', - type: 'tablet', - styles: { height: '800px', width: '800px' }, + name: "w800h800", + type: "tablet", + styles: { height: "800px", width: "800px" }, }, }, }, }, }, ], - title: 'file/Test Story', - } + title: "file/Test Story", + }, ); }); - it('successfully generates test results with mapped source entries', async () => { + it("successfully generates test results with mapped source entries", async () => { const expectedMappedJson = { childNodes: [ { type: NodeType.Element, attributes: { - src: '/home/index.html', + src: "/home/index.html", }, }, { type: NodeType.Element, attributes: { - src: '/img-fe2b41833610050d950fb9112407d3b3.png', + src: "/img-fe2b41833610050d950fb9112407d3b3.png", }, }, ], @@ -112,9 +114,9 @@ describe('writeTestResult', () => { await writeTestResult( // the default output directory in playwright { - titlePath: ['file.spec.ts', 'Toy Story'], - outputDir: resolve('test-results'), - pageUrl: 'http://localhost:3000/', + titlePath: ["file.spec.ts", "Toy Story"], + outputDir: resolve("test-results"), + pageUrl: "http://localhost:3000/", }, { home: { @@ -123,17 +125,17 @@ describe('writeTestResult', () => { }, }, { - 'http://localhost:3000/home/': { + "http://localhost:3000/home/": { statusCode: 200, body: Buffer.from(JSON.stringify(snapshotJson)), }, - 'http://localhost:3000/img?src=some-path': { + "http://localhost:3000/img?src=some-path": { statusCode: 200, - body: Buffer.from('image'), - contentType: 'image/png', + body: Buffer.from("image"), + contentType: "image/png", }, }, - {} + {}, ); expect(filePaths.ensureDir).toHaveBeenCalledTimes(1); @@ -141,19 +143,19 @@ describe('writeTestResult', () => { expect(filePaths.outputFile).toHaveBeenCalledTimes(3); expect(filePaths.outputFile).toHaveBeenCalledWith( resolve( - './test-results/chromatic-archives/archive/file-toy-story-home.w800h800.snapshot.json' + "./test-results/chromatic-archives/archive/file-toy-story-home.w800h800.snapshot.json", ), - JSON.stringify({ snapshot: expectedMappedJson }) + JSON.stringify({ snapshot: expectedMappedJson }), ); }); - it('stores archives in custom directory', async () => { + it("stores archives in custom directory", async () => { await writeTestResult( { - titlePath: ['file.spec.ts', 'Test Story'], + titlePath: ["file.spec.ts", "Test Story"], // simulates setting a custom output directory in Playwright - outputDir: resolve('some-custom-directory/directory'), - pageUrl: 'http://localhost:3000/', + outputDir: resolve("some-custom-directory/directory"), + pageUrl: "http://localhost:3000/", }, { home: { @@ -161,45 +163,45 @@ describe('writeTestResult', () => { viewport: { height: 800, width: 800 }, }, }, - { 'http://localhost:3000/home': { statusCode: 200, body: Buffer.from('Chromatic') } }, - {} + { "http://localhost:3000/home": { statusCode: 200, body: Buffer.from("Chromatic") } }, + {}, ); expect(filePaths.ensureDir).toHaveBeenCalledTimes(1); expect(filePaths.outputFile).toHaveBeenCalledTimes(2); expect(filePaths.outputJSONFile).toHaveBeenCalledTimes(1); expect(filePaths.outputJSONFile).toHaveBeenCalledWith( - resolve('./some-custom-directory/directory/chromatic-archives/file-test-story.stories.json'), - expect.anything() + resolve("./some-custom-directory/directory/chromatic-archives/file-test-story.stories.json"), + expect.anything(), ); }); - it('sanitizes story titles', async () => { + it("sanitizes story titles", async () => { await writeTestResult( { - titlePath: ['src/components/accordion.test.tsx', '', 'opens and closes'], - outputDir: resolve('test-results'), - pageUrl: 'http://localhost:3000/', + titlePath: ["src/components/accordion.test.tsx", "", "opens and closes"], + outputDir: resolve("test-results"), + pageUrl: "http://localhost:3000/", }, { home: { snapshot: Buffer.from(JSON.stringify({})), viewport: { height: 800, width: 800 } }, }, {}, - {} + {}, ); const { title } = vi.mocked(filePaths.outputJSONFile).mock.calls[0][1]; - expect(title).toBe('src/components/accordion//opens and closes'); + expect(title).toBe("src/components/accordion//opens and closes"); }); - describe('archive file system path windows', () => { - it('handles system paths', async () => { + describe("archive file system path windows", () => { + it("handles system paths", async () => { await writeTestResult( { - titlePath: ['file.spec.ts', 'Test Story'], + titlePath: ["file.spec.ts", "Test Story"], // simulates a system path (for windows) outputDir: - 'C:/Users/testuser/dev/test-results/dist/.playwright/apps/web-react-demo/test-output/', - pageUrl: 'http://localhost:3000/', + "C:/Users/testuser/dev/test-results/dist/.playwright/apps/web-react-demo/test-output/", + pageUrl: "http://localhost:3000/", }, { home: { @@ -208,29 +210,29 @@ describe('writeTestResult', () => { }, }, { - 'http://localhost:3000/@fs/C:/Users/testuser/dev/test-results/node_modules/vite/dist/client/index.html': + "http://localhost:3000/@fs/C:/Users/testuser/dev/test-results/node_modules/vite/dist/client/index.html": { statusCode: 200, - body: Buffer.from('Chromatic'), + body: Buffer.from("Chromatic"), }, }, - {} + {}, ); expect(filePaths.outputFile).toHaveBeenCalledWith( - 'C:/Users/testuser/dev/test-results/dist/.playwright/apps/web-react-demo/test-output/chromatic-archives/archive/fs/C/Users/testuser/dev/test-results/node_modules/vite/dist/client/index.html', - Buffer.from('Chromatic') + "C:/Users/testuser/dev/test-results/dist/.playwright/apps/web-react-demo/test-output/chromatic-archives/archive/fs/C/Users/testuser/dev/test-results/node_modules/vite/dist/client/index.html", + Buffer.from("Chromatic"), ); }); }); - describe('smart story naming', () => { - it('derives story title from test info, using all of the title path', async () => { + describe("smart story naming", () => { + it("derives story title from test info, using all of the title path", async () => { await writeTestResult( { - titlePath: ['file.spec.ts', 'A grouping', 'Test Story'], - outputDir: resolve('test-results'), - pageUrl: 'http://localhost:3000/', + titlePath: ["file.spec.ts", "A grouping", "Test Story"], + outputDir: resolve("test-results"), + pageUrl: "http://localhost:3000/", }, { home: { @@ -238,27 +240,27 @@ describe('writeTestResult', () => { viewport: { height: 800, width: 800 }, }, }, - { 'http://localhost:3000/home': { statusCode: 200, body: Buffer.from('Chromatic') } }, - {} + { "http://localhost:3000/home": { statusCode: 200, body: Buffer.from("Chromatic") } }, + {}, ); expect(filePaths.outputJSONFile).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - title: 'file/A grouping/Test Story', - }) + title: "file/A grouping/Test Story", + }), ); }); - it('preserves dots in directories, describe blocks, and test titles', async () => { + it("preserves dots in directories, describe blocks, and test titles", async () => { await writeTestResult( { titlePath: [ - 'a.directory/file.spec.ts', - '.someFunction', - '.someFunction() calls something', + "a.directory/file.spec.ts", + ".someFunction", + ".someFunction() calls something", ], - outputDir: resolve('test-results'), - pageUrl: 'http://localhost:3000/', + outputDir: resolve("test-results"), + pageUrl: "http://localhost:3000/", }, { home: { @@ -266,23 +268,23 @@ describe('writeTestResult', () => { viewport: { height: 800, width: 800 }, }, }, - { 'http://localhost:3000/home': { statusCode: 200, body: Buffer.from('Chromatic') } }, - {} + { "http://localhost:3000/home": { statusCode: 200, body: Buffer.from("Chromatic") } }, + {}, ); expect(filePaths.outputJSONFile).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - title: 'a.directory/file/.someFunction/.someFunction() calls something', - }) + title: "a.directory/file/.someFunction/.someFunction() calls something", + }), ); }); - it('preserves dots in file name, besides file extension (Playwright)', async () => { + it("preserves dots in file name, besides file extension (Playwright)", async () => { await writeTestResult( { - titlePath: ['some.file.spec.ts', 'Test Story'], - outputDir: resolve('test-results'), - pageUrl: 'http://localhost:3000/', + titlePath: ["some.file.spec.ts", "Test Story"], + outputDir: resolve("test-results"), + pageUrl: "http://localhost:3000/", }, { home: { @@ -290,23 +292,23 @@ describe('writeTestResult', () => { viewport: { height: 800, width: 800 }, }, }, - { 'http://localhost:3000/home': { statusCode: 200, body: Buffer.from('Chromatic') } }, - {} + { "http://localhost:3000/home": { statusCode: 200, body: Buffer.from("Chromatic") } }, + {}, ); expect(filePaths.outputJSONFile).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - title: 'some.file/Test Story', - }) + title: "some.file/Test Story", + }), ); }); - it('preserves dots in file name, besides file extension (Cypress)', async () => { + it("preserves dots in file name, besides file extension (Cypress)", async () => { await writeTestResult( { - titlePath: ['some.file.cy.ts', 'Test Story'], - outputDir: resolve('test-results'), - pageUrl: 'http://localhost:3000/', + titlePath: ["some.file.cy.ts", "Test Story"], + outputDir: resolve("test-results"), + pageUrl: "http://localhost:3000/", }, { home: { @@ -314,23 +316,23 @@ describe('writeTestResult', () => { viewport: { height: 800, width: 800 }, }, }, - { 'http://localhost:3000/home': { statusCode: 200, body: Buffer.from('Chromatic') } }, - {} + { "http://localhost:3000/home": { statusCode: 200, body: Buffer.from("Chromatic") } }, + {}, ); expect(filePaths.outputJSONFile).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - title: 'some.file/Test Story', - }) + title: "some.file/Test Story", + }), ); }); - it('removes file extension, even if .spec or .cy are not used', async () => { + it("removes file extension, even if .spec or .cy are not used", async () => { await writeTestResult( { - titlePath: ['file.ts', 'Test Story'], - outputDir: resolve('test-results'), - pageUrl: 'http://localhost:3000/', + titlePath: ["file.ts", "Test Story"], + outputDir: resolve("test-results"), + pageUrl: "http://localhost:3000/", }, { home: { @@ -338,14 +340,14 @@ describe('writeTestResult', () => { viewport: { height: 800, width: 800 }, }, }, - { 'http://localhost:3000/home': { statusCode: 200, body: Buffer.from('Chromatic') } }, - {} + { "http://localhost:3000/home": { statusCode: 200, body: Buffer.from("Chromatic") } }, + {}, ); expect(filePaths.outputJSONFile).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - title: 'file/Test Story', - }) + title: "file/Test Story", + }), ); }); }); diff --git a/packages/shared/src/write-archive/index.ts b/packages/shared/src/write-archive/index.ts index c3b6b8b2..5838c356 100644 --- a/packages/shared/src/write-archive/index.ts +++ b/packages/shared/src/write-archive/index.ts @@ -1,13 +1,14 @@ -import { join } from 'node:path'; -import { existsSync } from 'node:fs'; -import { outputFile, ensureDir, outputJSONFile } from '../utils/filePaths'; -import { logger } from '../utils/logger'; -import { ArchiveFile } from './archive-file'; -import { DOMSnapshot } from './dom-snapshot'; -import type { ResourceArchive } from '../resource-archiver'; -import type { ChromaticStorybookParameters, DOMSnapshots } from '../types'; -import { snapshotFileName, snapshotId } from './snapshot-files'; -import { createStories, storiesFileName } from './stories-files'; +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +import type { ResourceArchive } from "../resource-archiver"; +import type { ChromaticStorybookParameters, DOMSnapshots } from "../types"; +import { outputFile, ensureDir, outputJSONFile } from "../utils/filePaths"; +import { logger } from "../utils/logger"; +import { ArchiveFile } from "./archive-file"; +import { DOMSnapshot } from "./dom-snapshot"; +import { snapshotFileName, snapshotId } from "./snapshot-files"; +import { createStories, storiesFileName } from "./stories-files"; // We write a collection of DOM snapshots and a resource archive in the following locations: // .stories.json @@ -30,7 +31,7 @@ export async function writeTestResult( domSnapshots: DOMSnapshots, archive: ResourceArchive, chromaticStorybookParams: RequiredButOptional, - testRunner?: 'cypress' | 'playwright' | 'vitest' + testRunner?: "cypress" | "playwright" | "vitest", ) { const { titlePath, outputDir, pageUrl } = e2eTestInfo; // remove the test file extensions (.spec.ts|ts, .cy.ts|js), preserving other periods in directory, file name, or test titles @@ -39,21 +40,21 @@ export async function writeTestResult( // possible extensions: // playwright: https://playwright.dev/docs/test-configuration#filtering-tests // cypress: https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Spec-files - pathPart.replace(/\.(ts|js|mjs|cjs|tsx|jsx|cjsx|coffee)$/, '').replace(/\.(spec|test|cy)$/, '') + pathPart.replace(/\.(ts|js|mjs|cjs|tsx|jsx|cjsx|coffee)$/, "").replace(/\.(spec|test|cy)$/, ""), ); // in Storybook, `/` splits the title out into hierarchies (folders) const title = titlePathWithoutFileExtensions - .join('/') + .join("/") // Make sure we don't end up with folders with just special characters // Transforms paths like "src/components/accordion//opens and closes" to "src/components/accordion//opens and closes " // eslint-disable-next-line no-useless-escape - .replace(/\/([ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\]+)\//gi, '$1/'); + .replace(/\/([ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\]+)\//gi, "$1/"); - const finalOutputDir = join(outputDir, 'chromatic-archives'); + const finalOutputDir = join(outputDir, "chromatic-archives"); - const archiveDir = join(finalOutputDir, 'archive'); + const archiveDir = join(finalOutputDir, "archive"); - await ensureDir(archiveDir); + ensureDir(archiveDir); logger.log(`Writing test results for "${title}"`); @@ -63,7 +64,7 @@ export async function writeTestResult( await Promise.all( Object.entries(archive).map(async ([url, response]) => { - if ('error' in response) return; + if ("error" in response) return; const archiveFile = new ArchiveFile(url, response, pageUrl); const origSrcPath = archiveFile.originalSrc(); @@ -77,7 +78,7 @@ export async function writeTestResult( if (!existsSync(responsePath)) { await outputFile(responsePath, response.body); } - }) + }), ); await Promise.all( @@ -89,14 +90,14 @@ export async function writeTestResult( const snapshotFile = snapshotFileName(snapshotId(title, name), domSnapshot.viewport); await outputFile(join(archiveDir, snapshotFile), mappedSnapshot); - }) + }), ); - const storiesFile = join(finalOutputDir, storiesFileName(title, testRunner !== 'vitest')); + const storiesFile = join(finalOutputDir, storiesFileName(title, testRunner !== "vitest")); const storiesJson = createStories(title, domSnapshots, chromaticStorybookParams); await outputJSONFile(storiesFile, storiesJson); - const errors = Object.entries(archive).filter(([, r]) => 'error' in r); + const errors = Object.entries(archive).filter(([, r]) => "error" in r); if (errors.length > 0) { logger.log(`Encountered ${errors.length} errors archiving resources, writing to 'errors.json'`); await outputJSONFile(join(archiveDir, `errors.json`), { diff --git a/packages/shared/src/write-archive/snapshot-files.test.ts b/packages/shared/src/write-archive/snapshot-files.test.ts index 28f15040..4cd7a827 100644 --- a/packages/shared/src/write-archive/snapshot-files.test.ts +++ b/packages/shared/src/write-archive/snapshot-files.test.ts @@ -1,38 +1,39 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import * as snapshotFiles from './snapshot-files'; +import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock('fs/promises'); +import * as snapshotFiles from "./snapshot-files"; + +vi.mock("fs/promises"); beforeEach(() => { vi.resetAllMocks(); }); -describe('snapshotId', () => { - it('sanitizes the snapshot ID', () => { +describe("snapshotId", () => { + it("sanitizes the snapshot ID", () => { const snapshotId = snapshotFiles.snapshotId( - 'a title *() with $%& chars', - 'a snapshot name *() with $%& chars' + "a title *() with $%& chars", + "a snapshot name *() with $%& chars", ); - expect(snapshotId).toEqual('a-title-with-chars-a-snapshot-name-with-chars'); + expect(snapshotId).toEqual("a-title-with-chars-a-snapshot-name-with-chars"); }); - it('truncates long snashot IDs', () => { + it("truncates long snashot IDs", () => { const title = - 'this title has 260 chars exactly i know because i counted and that is too big for a file system blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah ok this right here this is the end'; - const snapshotName = 'and this is a snapshot name these should not be too long usually'; + "this title has 260 chars exactly i know because i counted and that is too big for a file system blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah ok this right here this is the end"; + const snapshotName = "and this is a snapshot name these should not be too long usually"; const snapshotId = snapshotFiles.snapshotId(title, snapshotName); expect(snapshotId.length).toEqual(230); - expect(snapshotId).toMatch(new RegExp('^this-title-has-.*blah-blah-[a-z0-9]{4}$')); + expect(snapshotId).toMatch(new RegExp("^this-title-has-.*blah-blah-[a-z0-9]{4}$")); }); }); -describe('snapshotFileName', () => { - it('generates a filename with the id and viewport', () => { - const fileName = snapshotFiles.snapshotFileName('some-id', { width: 500, height: 720 }); +describe("snapshotFileName", () => { + it("generates a filename with the id and viewport", () => { + const fileName = snapshotFiles.snapshotFileName("some-id", { width: 500, height: 720 }); - expect(fileName).toEqual('some-id.w500h720.snapshot.json'); + expect(fileName).toEqual("some-id.w500h720.snapshot.json"); }); }); diff --git a/packages/shared/src/write-archive/snapshot-files.ts b/packages/shared/src/write-archive/snapshot-files.ts index e64a3178..a59ab023 100644 --- a/packages/shared/src/write-archive/snapshot-files.ts +++ b/packages/shared/src/write-archive/snapshot-files.ts @@ -1,8 +1,8 @@ -import { Viewport, viewportToString } from '../utils/viewport'; -import { sanitize } from './storybook-sanitize'; -import { MAX_FILE_NAME_BYTE_LENGTH, truncateFileName } from '../utils/filePaths'; +import { MAX_FILE_NAME_BYTE_LENGTH, truncateFileName } from "../utils/filePaths"; +import { Viewport, viewportToString } from "../utils/viewport"; +import { sanitize } from "./storybook-sanitize"; -const SNAPSHOT_FILE_EXT = 'snapshot.json'; +const SNAPSHOT_FILE_EXT = "snapshot.json"; export function snapshotId(testTitle: string, snapshotName: string) { const fullSnapshotId = `${sanitize(testTitle)}-${sanitize(snapshotName)}`; @@ -16,5 +16,5 @@ export function snapshotId(testTitle: string, snapshotName: string) { export function snapshotFileName(snapshotId: string, viewport: Viewport) { const fileNameParts = [snapshotId, viewportToString(viewport), SNAPSHOT_FILE_EXT]; - return fileNameParts.join('.'); + return fileNameParts.join("."); } diff --git a/packages/shared/src/write-archive/stories-files.test.ts b/packages/shared/src/write-archive/stories-files.test.ts index 0d11031d..b349bd0a 100644 --- a/packages/shared/src/write-archive/stories-files.test.ts +++ b/packages/shared/src/write-archive/stories-files.test.ts @@ -1,8 +1,9 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ChromaticStorybookParameters } from '../types'; -import * as storiesFiles from './stories-files'; +import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock('fs/promises'); +import { ChromaticStorybookParameters } from "../types"; +import * as storiesFiles from "./stories-files"; + +vi.mock("fs/promises"); const vports = [ { width: 100, height: 1000 }, @@ -14,36 +15,36 @@ beforeEach(() => { vi.resetAllMocks(); }); -describe('storiesFileName', () => { - it('sanitizes the file name', () => { - const fileName = storiesFiles.storiesFileName('--a title *() with $%& chars---'); +describe("storiesFileName", () => { + it("sanitizes the file name", () => { + const fileName = storiesFiles.storiesFileName("--a title *() with $%& chars---"); expect(fileName).toEqual(`a-title-with-chars.stories.json`); }); - it('truncates long file names', () => { + it("truncates long file names", () => { const title = - 'this title has 260 chars exactly i know because i counted and that is too big for a file system blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah ok this right here this is the end'; + "this title has 260 chars exactly i know because i counted and that is too big for a file system blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah ok this right here this is the end"; expect(title.length).toBeGreaterThan(255); const fileName = storiesFiles.storiesFileName(title); expect(fileName.length).toEqual(230); - expect(fileName).toMatch(new RegExp('^this-title-has-.*blah-bl[a-z0-9]{4}.stories.json$')); + expect(fileName).toMatch(new RegExp("^this-title-has-.*blah-bl[a-z0-9]{4}.stories.json$")); }); - it('replaces newlines with -', () => { - const title = '\n\n\r\rThere\nShould\rBe\r\nNo\n\rNewlines\r\r\n\n'; + it("replaces newlines with -", () => { + const title = "\n\n\r\rThere\nShould\rBe\r\nNo\n\rNewlines\r\r\n\n"; const filename = storiesFiles.storiesFileName(title); - expect(filename).toEqual('there-should-be-no-newlines.stories.json'); + expect(filename).toEqual("there-should-be-no-newlines.stories.json"); }); }); -describe('createStories', () => { - it('creates stories file JSON from DOM snapshots', () => { - const title = 'some test title'; +describe("createStories", () => { + it("creates stories file JSON from DOM snapshots", () => { + const title = "some test title"; const domSnapshots = { - 'snapshot 1': { snapshot: Buffer.from('n/a'), viewport: { width: 100, height: 200 } }, - 'another snapshot': { snapshot: Buffer.from('n/a'), viewport: { width: 300, height: 400 } }, + "snapshot 1": { snapshot: Buffer.from("n/a"), viewport: { width: 100, height: 200 } }, + "another snapshot": { snapshot: Buffer.from("n/a"), viewport: { width: 300, height: 400 } }, }; const chromaticParams: ChromaticStorybookParameters = { delay: 200, @@ -56,29 +57,29 @@ describe('createStories', () => { title, stories: [ { - name: 'snapshot 1', - globals: { viewport: 'w100h200' }, + name: "snapshot 1", + globals: { viewport: "w100h200" }, parameters: { - __id: 'some-test-title--snapshot-1', - server: { id: 'some-test-title-snapshot-1' }, + __id: "some-test-title--snapshot-1", + server: { id: "some-test-title-snapshot-1" }, chromatic: { delay: 200, pauseAnimationAtEnd: true, modes: { w100h200: { - viewport: 'w100h200', + viewport: "w100h200", }, }, }, viewport: { - defaultViewport: 'w100h200', + defaultViewport: "w100h200", options: { w100h200: { - name: 'w100h200', - type: 'mobile', + name: "w100h200", + type: "mobile", styles: { - height: '200px', - width: '100px', + height: "200px", + width: "100px", }, }, }, @@ -86,29 +87,29 @@ describe('createStories', () => { }, }, { - name: 'another snapshot', - globals: { viewport: 'w300h400' }, + name: "another snapshot", + globals: { viewport: "w300h400" }, parameters: { - __id: 'some-test-title--another-snapshot', - server: { id: 'some-test-title-another-snapshot' }, + __id: "some-test-title--another-snapshot", + server: { id: "some-test-title-another-snapshot" }, chromatic: { delay: 200, pauseAnimationAtEnd: true, modes: { w300h400: { - viewport: 'w300h400', + viewport: "w300h400", }, }, }, viewport: { - defaultViewport: 'w300h400', + defaultViewport: "w300h400", options: { w300h400: { - name: 'w300h400', - type: 'mobile', + name: "w300h400", + type: "mobile", styles: { - height: '400px', - width: '300px', + height: "400px", + width: "300px", }, }, }, @@ -119,77 +120,77 @@ describe('createStories', () => { }); }); - it('collapses newlines in title', () => { + it("collapses newlines in title", () => { const storiesFileJSON = storiesFiles.createStories( - '\n\n\r\rThere\nShould\rBe\r\nNo\n\rNewlines\r\r\n\n', + "\n\n\r\rThere\nShould\rBe\r\nNo\n\rNewlines\r\r\n\n", + {}, {}, - {} ); - expect(storiesFileJSON.title).toEqual('There Should Be No Newlines'); + expect(storiesFileJSON.title).toEqual("There Should Be No Newlines"); }); - it('collapses newlines in story names', () => { + it("collapses newlines in story names", () => { const storiesFileJSON = storiesFiles.createStories( - 'Some Title', + "Some Title", { - '\n\n\r\rSnapshot\nName\rWith\r\nNewlines\n\r\r\n': { - snapshot: Buffer.from('n/a'), + "\n\n\r\rSnapshot\nName\rWith\r\nNewlines\n\r\r\n": { + snapshot: Buffer.from("n/a"), viewport: { width: 100, height: 200 }, }, }, - {} + {}, ); - expect(storiesFileJSON.stories[0].name).toEqual('Snapshot Name With Newlines'); + expect(storiesFileJSON.stories[0].name).toEqual("Snapshot Name With Newlines"); }); }); -describe('buildStoryModesConfig', () => { - it('builds viewports config for storybook parameters', () => { +describe("buildStoryModesConfig", () => { + it("builds viewports config for storybook parameters", () => { const viewportsConfig = storiesFiles.buildStoryModesConfig(vports); expect(viewportsConfig).toEqual({ - w100h1000: { viewport: 'w100h1000' }, - w1200h100: { viewport: 'w1200h100' }, - w500h500: { viewport: 'w500h500' }, + w100h1000: { viewport: "w100h1000" }, + w1200h100: { viewport: "w1200h100" }, + w500h500: { viewport: "w500h500" }, }); }); }); -describe('buildStoryViewportsConfig', () => { - it('builds viewports config for storybook parameters', () => { +describe("buildStoryViewportsConfig", () => { + it("builds viewports config for storybook parameters", () => { const viewportsConfig = storiesFiles.buildStoryViewportsConfig(vports); expect(viewportsConfig).toEqual({ w100h1000: { - name: 'w100h1000', - type: 'mobile', + name: "w100h1000", + type: "mobile", styles: { - width: '100px', - height: '1000px', + width: "100px", + height: "1000px", }, }, w1200h100: { - name: 'w1200h100', - type: 'desktop', + name: "w1200h100", + type: "desktop", styles: { - width: '1200px', - height: '100px', + width: "1200px", + height: "100px", }, }, w500h500: { - name: 'w500h500', - type: 'mobile', + name: "w500h500", + type: "mobile", styles: { - width: '500px', - height: '500px', + width: "500px", + height: "500px", }, }, }); }); }); -describe('findDefaultViewport', () => { - it('returns the default viewport given a list', () => { +describe("findDefaultViewport", () => { + it("returns the default viewport given a list", () => { const defaultViewport = storiesFiles.findDefaultViewport(vports); expect(defaultViewport).toEqual({ width: 1200, height: 100 }); }); diff --git a/packages/shared/src/write-archive/stories-files.ts b/packages/shared/src/write-archive/stories-files.ts index 9a6ed7f0..215575ca 100644 --- a/packages/shared/src/write-archive/stories-files.ts +++ b/packages/shared/src/write-archive/stories-files.ts @@ -1,11 +1,12 @@ -import { storyNameFromExport, toId } from 'storybook/internal/csf'; -import type { ChromaticStorybookParameters, DOMSnapshots } from '../types'; -import { snapshotId } from './snapshot-files'; -import { collapseNewlines, sanitize } from './storybook-sanitize'; -import { Viewport, viewportToString } from '../utils/viewport'; -import { MAX_FILE_NAME_BYTE_LENGTH, truncateFileName } from '../utils/filePaths'; +import { storyNameFromExport, toId } from "storybook/internal/csf"; -const STORIES_FILE_EXT = 'stories.json'; +import type { ChromaticStorybookParameters, DOMSnapshots } from "../types"; +import { MAX_FILE_NAME_BYTE_LENGTH, truncateFileName } from "../utils/filePaths"; +import { Viewport, viewportToString } from "../utils/viewport"; +import { snapshotId } from "./snapshot-files"; +import { collapseNewlines, sanitize } from "./storybook-sanitize"; + +const STORIES_FILE_EXT = "stories.json"; export const uniqueId = { value: 1 }; @@ -15,10 +16,10 @@ export function storiesFileName(testTitle: string, overwriteDuplicateNames = tru // If multiple test runner project generate the same name, it's random which one ends up on file system if (!overwriteDuplicateNames) { - title += '-' + uniqueId.value++; + title += "-" + uniqueId.value++; } - const fileName = [title, STORIES_FILE_EXT].join('.'); + const fileName = [title, STORIES_FILE_EXT].join("."); // Leave room for built storybook extensions that may be added (like `-stories.iframe.bundle.js`) const maxByteLength = MAX_FILE_NAME_BYTE_LENGTH - 25; @@ -29,7 +30,7 @@ export function storiesFileName(testTitle: string, overwriteDuplicateNames = tru export function createStories( storyTitle: string, domSnapshots: DOMSnapshots, - chromaticStorybookParams: ChromaticStorybookParameters + chromaticStorybookParams: ChromaticStorybookParameters, ) { const title = collapseNewlines(storyTitle); @@ -98,14 +99,14 @@ export function buildStoryViewportsConfig(viewports: Viewport[]) { }, {}); } -function viewportAddonType(viewport: Viewport): 'mobile' | 'tablet' | 'desktop' | 'other' { +function viewportAddonType(viewport: Viewport): "mobile" | "tablet" | "desktop" | "other" { if (viewport.width < 600) { - return 'mobile'; + return "mobile"; } if (viewport.width < 1024) { - return 'tablet'; + return "tablet"; } - return 'desktop'; + return "desktop"; } // Finds a viewport to use as the default. diff --git a/packages/shared/src/write-archive/storybook-sanitize.ts b/packages/shared/src/write-archive/storybook-sanitize.ts index 2a3bd552..b1ebd272 100644 --- a/packages/shared/src/write-archive/storybook-sanitize.ts +++ b/packages/shared/src/write-archive/storybook-sanitize.ts @@ -5,13 +5,13 @@ export const sanitize = (string: string) => { string .toLowerCase() // eslint-disable-next-line no-useless-escape - .replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '-') - .replace(/[\r\n]/g, '-') - .replace(/-+/g, '-') - .replace(/^-+/, '') - .replace(/-+$/, '') + .replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "-") + .replace(/[\r\n]/g, "-") + .replace(/-+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, "") ); }; // Collapses CR/LF in a display title to a single space. Required for https://github.com/chromaui/chromatic-e2e/issues/369. -export const collapseNewlines = (title: string) => title.replace(/[\r\n]+/g, ' ').trim(); +export const collapseNewlines = (title: string) => title.replace(/[\r\n]+/g, " ").trim(); diff --git a/packages/shared/storybook-config/manager.ts b/packages/shared/storybook-config/manager.ts index 99af2406..bd3d761b 100644 --- a/packages/shared/storybook-config/manager.ts +++ b/packages/shared/storybook-config/manager.ts @@ -1,4 +1,4 @@ -import { addons } from 'storybook/manager-api'; +import { addons } from "storybook/manager-api"; addons.setConfig({ sidebar: { diff --git a/packages/shared/storybook-config/preview.ts b/packages/shared/storybook-config/preview.ts index c5e80fc1..401738dc 100644 --- a/packages/shared/storybook-config/preview.ts +++ b/packages/shared/storybook-config/preview.ts @@ -1,18 +1,18 @@ -import type { RenderContext, RenderToCanvas, WebRenderer } from 'storybook/internal/types'; -import type { serializedNodeWithId } from '@rrweb/types'; -import { NodeType } from '@rrweb/types'; -import { rebuild, createMirror, createCache } from '@chromaui/rrweb-snapshot'; -import { type SavedSnapshot } from '@chromatic-com/shared-e2e'; +import { type SavedSnapshot } from "@chromatic-com/shared-e2e"; import { isElement, isIframeElement, isIframeSerializedNode, -} from '@chromatic-com/shared-e2e/utils/nodes'; +} from "@chromatic-com/shared-e2e/utils/nodes"; +import { rebuild, createMirror, createCache } from "@chromaui/rrweb-snapshot"; +import type { serializedNodeWithId } from "@rrweb/types"; +import { NodeType } from "@rrweb/types"; +import type { RenderContext, RenderToCanvas, WebRenderer } from "storybook/internal/types"; const pageUrl = new URL(window.location.href); -pageUrl.pathname = ''; -pageUrl.search = ''; -const strippedUrl = pageUrl.toString().replace(/\/$/, ''); +pageUrl.pathname = ""; +pageUrl.search = ""; +const strippedUrl = pageUrl.toString().replace(/\/$/, ""); export interface RRWebFramework extends WebRenderer { component: undefined; @@ -20,11 +20,11 @@ export interface RRWebFramework extends WebRenderer { } const findHtmlNode = (node: serializedNodeWithId): serializedNodeWithId | undefined => { - if (node.type === NodeType.Element && node.tagName === 'html') { + if (node.type === NodeType.Element && node.tagName === "html") { return node; } - if ('childNodes' in node) { + if ("childNodes" in node) { return node.childNodes.find((childNode) => { return findHtmlNode(childNode); }); @@ -38,7 +38,7 @@ const findNodeById = (node: serializedNodeWithId, id: number): serializedNodeWit return node; } - if ('childNodes' in node) { + if ("childNodes" in node) { for (const childNode of node.childNodes) { const match = findNodeById(childNode, id); @@ -53,8 +53,8 @@ const findNodeById = (node: serializedNodeWithId, id: number): serializedNodeWit // NOTE: This is duplicated in the shared package due to bundling issues function snapshotFileName(snapshotId: string, viewport: string) { - const fileNameParts = [snapshotId, viewport, 'snapshot.json']; - return fileNameParts.join('.'); + const fileNameParts = [snapshotId, viewport, "snapshot.json"]; + return fileNameParts.join("."); } async function fetchSnapshot(context: RenderContext): Promise { @@ -63,7 +63,7 @@ async function fetchSnapshot(context: RenderContext): Promise { +it("/embedded/node_modules has not changed", () => { const { hash, fileCount } = generateEmbeddedChecksum(import.meta.dirname); expect(fileCount).toMatchInlineSnapshot(`8198`); expect(hash).toMatchInlineSnapshot( - `"fb7a3c92fbe7930617d966322c4b20b3918a3e197422c2850b6f8838a9d8ad8c"` + `"fb7a3c92fbe7930617d966322c4b20b3918a3e197422c2850b6f8838a9d8ad8c"`, ); }); -it('/embedded/node_modules tree snapshot', () => { +it("/embedded/node_modules tree snapshot", () => { expect(collectEmbeddedFiles(import.meta.dirname)).toMatchInlineSnapshot(` [ "@babel/code-frame@7.29.7", diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 92e7e2f8..69f14e41 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -2,17 +2,24 @@ "name": "@chromatic-com/vitest", "version": "1.0.1", "description": "Chromatic Visual Regression Testing for Vitest", + "license": "MIT", + "author": "Chromatic ", "repository": { "type": "git", "url": "git+https://github.com/chromaui/chromatic-e2e.git", "directory": "packages/vitest" }, - "author": "Chromatic ", - "license": "MIT", - "type": "module", - "engines": { - "node": ">=22" + "bin": { + "archive-storybook": "dist/bin/archive-storybook.mjs", + "build-archive-storybook": "dist/bin/build-archive-storybook.mjs" }, + "files": [ + "dist", + "embedded" + ], + "type": "module", + "main": "dist/index.mjs", + "types": "dist/index.d.mts", "exports": { ".": { "types": "./dist/index.d.mts", @@ -26,16 +33,9 @@ "./bin/build-archive-storybook": "./dist/bin/build-archive-storybook.mjs", "./package.json": "./package.json" }, - "main": "dist/index.mjs", - "types": "dist/index.d.mts", - "bin": { - "archive-storybook": "dist/bin/archive-storybook.mjs", - "build-archive-storybook": "dist/bin/build-archive-storybook.mjs" + "publishConfig": { + "access": "public" }, - "files": [ - "dist", - "embedded" - ], "scripts": { "build": "tsdown", "test:unit": "pnpm --filter @chromaui/chromatic-e2e run test:unit --project 'Vitest Unit' --project 'Vitest Browser'", @@ -43,8 +43,8 @@ }, "dependencies": { "@chromaui/rrweb-snapshot": "2.0.0-alpha.19-noAbsolute", - "storybook": "10.2.13", "std-env": "^4.2.0", + "storybook": "10.2.13", "tinyexec": "^1.2.4", "tinyrainbow": "^3.1.0" }, @@ -63,7 +63,7 @@ "peerDependencies": { "vitest": "^4 || ^5" }, - "publishConfig": { - "access": "public" + "engines": { + "node": ">=22" } } diff --git a/packages/vitest/src/bin/archive-storybook.ts b/packages/vitest/src/bin/archive-storybook.ts index 7d5e90fe..817525b9 100644 --- a/packages/vitest/src/bin/archive-storybook.ts +++ b/packages/vitest/src/bin/archive-storybook.ts @@ -1,23 +1,25 @@ #!/usr/bin/env node -import { archiveStorybook } from '@chromatic-com/shared-e2e/archive-storybook'; -import { archivesDir } from '@chromatic-com/shared-e2e/utils/filePaths'; -import path from 'path'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; -import { type EventType, type TelemetryEvent, trackCliEvent } from '../node/telemetry'; +import path from "node:path"; + +import { archiveStorybook } from "@chromatic-com/shared-e2e/archive-storybook"; +import { archivesDir } from "@chromatic-com/shared-e2e/utils/filePaths"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; +import { type EventType, type TelemetryEvent, trackCliEvent } from "../node/telemetry"; // Discard first two entries (exec path and file path) const args = process.argv.slice(2); -const configDir = path.resolve(import.meta.dirname, '../storybook-config'); +const configDir = path.resolve(import.meta.dirname, "../storybook-config"); let queue = Promise.resolve(); try { - trackEvent({ eventType: 'storybook_dev_started', level: 'info', payload: {} }); + trackEvent({ eventType: "storybook_dev_started", level: "info", payload: {} }); await archiveStorybook(args, configDir, DEFAULT_OUTPUT_DIR, { onArchivesCheck }); } catch (err) { - trackEvent({ eventType: 'storybook_dev_failed', level: 'error', payload: { error: err } }); + trackEvent({ eventType: "storybook_dev_failed", level: "error", payload: { error: err } }); // Throwing the error results in a large output of minified code and a stacktrace that is // likely not helpful to users, so this should hide the noise. @@ -31,20 +33,20 @@ await queue; function trackEvent(event: TelemetryEvent) { queue = queue.then(() => trackCliEvent(event, { outputDirectory: path.dirname(archivesDir(DEFAULT_OUTPUT_DIR)) }).catch( - () => {} // Ignore telemetry errors - ) + () => {}, // Ignore telemetry errors + ), ); } function onArchivesCheck(error?: unknown) { trackEvent({ - eventType: 'archives_resolved', - level: error ? 'error' : 'info', + eventType: "archives_resolved", + level: error ? "error" : "info", payload: { success: error == undefined, error, isCustomLocation: process.env.CHROMATIC_ARCHIVE_LOCATION != undefined, - command: 'archiveStorybook', + command: "archiveStorybook", chromaticProjectId: undefined, }, }); diff --git a/packages/vitest/src/bin/build-archive-storybook.ts b/packages/vitest/src/bin/build-archive-storybook.ts index 91d48eb3..ecc9421b 100644 --- a/packages/vitest/src/bin/build-archive-storybook.ts +++ b/packages/vitest/src/bin/build-archive-storybook.ts @@ -1,39 +1,41 @@ #!/usr/bin/env node -import { buildArchiveStorybook } from '@chromatic-com/shared-e2e/archive-storybook'; -import { archivesDir } from '@chromatic-com/shared-e2e/utils/filePaths'; -import path from 'path'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; -import { type EventType, type TelemetryEvent, trackCliEvent } from '../node/telemetry'; +import path from "node:path"; + +import { buildArchiveStorybook } from "@chromatic-com/shared-e2e/archive-storybook"; +import { archivesDir } from "@chromatic-com/shared-e2e/utils/filePaths"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; +import { type EventType, type TelemetryEvent, trackCliEvent } from "../node/telemetry"; // Discard first two entries (exec path and file path) const args = process.argv.slice(2); -const configDir = path.resolve(import.meta.dirname, '../storybook-config'); +const configDir = path.resolve(import.meta.dirname, "../storybook-config"); // These environment variables are set by chromatic-cli: -const isCalledFromCLI = process.env.STORYBOOK_INVOKED_BY === 'chromatic'; -const chromaticProjectId = process.env.CHROMATIC_PROJECT_ID || 'unknown'; +const isCalledFromCLI = process.env.STORYBOOK_INVOKED_BY === "chromatic"; +const chromaticProjectId = process.env.CHROMATIC_PROJECT_ID || "unknown"; let queue = Promise.resolve(); try { trackEvent({ - eventType: 'storybook_build_started', - level: 'info', + eventType: "storybook_build_started", + level: "info", payload: { isCalledFromCLI, chromaticProjectId }, }); await buildArchiveStorybook(args, configDir, DEFAULT_OUTPUT_DIR, { onArchivesCheck }); trackEvent({ - eventType: 'storybook_build_completed', - level: 'info', + eventType: "storybook_build_completed", + level: "info", payload: { success: true, error: undefined, chromaticProjectId }, }); } catch (err) { trackEvent({ - eventType: 'storybook_build_completed', - level: 'error', + eventType: "storybook_build_completed", + level: "error", payload: { success: false, error: err, chromaticProjectId }, }); @@ -49,20 +51,20 @@ await queue; function trackEvent(event: TelemetryEvent) { queue = queue.then(() => trackCliEvent(event, { outputDirectory: path.dirname(archivesDir(DEFAULT_OUTPUT_DIR)) }).catch( - () => {} // Ignore telemetry errors - ) + () => {}, // Ignore telemetry errors + ), ); } function onArchivesCheck(error?: unknown) { trackEvent({ - eventType: 'archives_resolved', - level: error ? 'error' : 'info', + eventType: "archives_resolved", + level: error ? "error" : "info", payload: { success: error == undefined, error, isCustomLocation: process.env.CHROMATIC_ARCHIVE_LOCATION != undefined, - command: 'buildArchiveStorybook', + command: "buildArchiveStorybook", chromaticProjectId, }, }); diff --git a/packages/vitest/src/browser/getCurrentTest.ts b/packages/vitest/src/browser/getCurrentTest.ts index 1edeccae..07f5535f 100644 --- a/packages/vitest/src/browser/getCurrentTest.ts +++ b/packages/vitest/src/browser/getCurrentTest.ts @@ -1,8 +1,9 @@ -import * as vitest from 'vitest'; -import { InternalTestContext } from '../types'; +import * as vitest from "vitest"; + +import { InternalTestContext } from "../types"; export type Test = ReturnType & - InternalTestContext['task']; + InternalTestContext["task"]; const hooks = await resolveHooks(); @@ -21,5 +22,5 @@ async function resolveHooks() { } // Fallback to older API. Using this with 4.1.0 logs deprecation warning. - return await import('vitest/suite'); + return await import("vitest/suite"); } diff --git a/packages/vitest/src/browser/isChromium.ts b/packages/vitest/src/browser/isChromium.ts index 24792b29..ad503655 100644 --- a/packages/vitest/src/browser/isChromium.ts +++ b/packages/vitest/src/browser/isChromium.ts @@ -1,5 +1,5 @@ -import { server } from 'vitest/browser'; +import { server } from "vitest/browser"; export function isChromium(): boolean { - return server.browser === 'chromium'; + return server.browser === "chromium"; } diff --git a/packages/vitest/src/browser/public/configure.test.ts b/packages/vitest/src/browser/public/configure.test.ts index 3ab7a72d..f1e78764 100644 --- a/packages/vitest/src/browser/public/configure.test.ts +++ b/packages/vitest/src/browser/public/configure.test.ts @@ -1,17 +1,18 @@ -import { describe, expect, test, vi } from 'vitest'; -import * as shared from '@chromatic-com/shared-e2e'; -import { runFixture } from '../../../test/utils/node'; +import * as shared from "@chromatic-com/shared-e2e"; +import { describe, expect, test, vi } from "vitest"; -vi.mock('@chromatic-com/shared-e2e'); +import { runFixture } from "../../../test/utils/node"; + +vi.mock("@chromatic-com/shared-e2e"); vi.mocked(shared.writeTestResult).mockImplementation(() => - Promise.resolve({ storiesFile: 'test.stories.json' }) + Promise.resolve({ storiesFile: "test.stories.json" }), ); -describe('configure({ disableAutoSnapshot })', () => { +describe("configure({ disableAutoSnapshot })", () => { /** See {@link file://./../../../test/fixtures/disable-autosnapshot.test.ts} */ - const include = ['disable-autosnapshot.test.ts']; + const include = ["disable-autosnapshot.test.ts"]; - test('by default does not disable anything', async () => { + test("by default does not disable anything", async () => { await runFixture({ include }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` @@ -25,19 +26,19 @@ describe('configure({ disableAutoSnapshot })', () => { `); }); - test('{ disableAutoSnapshot: true } on module level affects all tests', async () => { + test("{ disableAutoSnapshot: true } on module level affects all tests", async () => { await runFixture({ include, - provide: { disableAutoSnapshot: 'module' }, + provide: { disableAutoSnapshot: "module" }, }); expect(getSnapshottedTests()).toMatchInlineSnapshot(`[]`); }); - test('{ disableAutoSnapshot: true } on suite level', async () => { + test("{ disableAutoSnapshot: true } on suite level", async () => { await runFixture({ include, - provide: { disableAutoSnapshot: 'describe' }, + provide: { disableAutoSnapshot: "describe" }, }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` @@ -48,10 +49,10 @@ describe('configure({ disableAutoSnapshot })', () => { `); }); - test('{ disableAutoSnapshot: true } on nested suite level', async () => { + test("{ disableAutoSnapshot: true } on nested suite level", async () => { await runFixture({ include, - provide: { disableAutoSnapshot: 'describe-nested' }, + provide: { disableAutoSnapshot: "describe-nested" }, }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` @@ -64,19 +65,19 @@ describe('configure({ disableAutoSnapshot })', () => { `); }); - test('{ disableAutoSnapshot: true } on test level', async () => { + test("{ disableAutoSnapshot: true } on test level", async () => { await runFixture({ include, - provide: { disableAutoSnapshot: 'test' }, + provide: { disableAutoSnapshot: "test" }, }); expect(getSnapshottedTests()).toMatchInlineSnapshot(`[]`); }); - test('{ disableAutoSnapshot: true } on a single test', async () => { + test("{ disableAutoSnapshot: true } on a single test", async () => { await runFixture({ include, - provide: { disableAutoSnapshot: 'test-second' }, + provide: { disableAutoSnapshot: "test-second" }, }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` @@ -90,26 +91,26 @@ describe('configure({ disableAutoSnapshot })', () => { }); }); -describe('configure(ChromaticConfig)', () => { +describe("configure(ChromaticConfig)", () => { /** See {@link file://./../../../test/fixtures/configure-options.test.ts} */ - const include = ['configure-options.test.ts']; + const include = ["configure-options.test.ts"]; - test('configure() on module level affects all tests', async () => { + test("configure() on module level affects all tests", async () => { const configureOptions = { delay: 300, diffIncludeAntiAliasing: true, diffThreshold: 0.5, - forcedColors: 'active', + forcedColors: "active", pauseAnimationAtEnd: true, - prefersReducedMotion: 'reduce', + prefersReducedMotion: "reduce", cropToViewport: true, - ignoreSelectors: ['.ignore-me'], + ignoreSelectors: [".ignore-me"], }; await runFixture({ include, provide: { - configureScope: 'module', + configureScope: "module", configureOptions, }, }); @@ -122,12 +123,12 @@ describe('configure(ChromaticConfig)', () => { } }); - test('configure() on suite level', async () => { + test("configure() on suite level", async () => { await runFixture({ include, provide: { - configureScope: 'describe', - configureOptions: { delay: 300, ignoreSelectors: ['.example'] }, + configureScope: "describe", + configureOptions: { delay: 300, ignoreSelectors: [".example"] }, }, }); @@ -157,11 +158,11 @@ describe('configure(ChromaticConfig)', () => { `); }); - test('configure() on nested suite level', async () => { + test("configure() on nested suite level", async () => { await runFixture({ include, provide: { - configureScope: 'describe-nested', + configureScope: "describe-nested", configureOptions: { cropToViewport: true, diffThreshold: 0.5 }, }, }); @@ -180,12 +181,12 @@ describe('configure(ChromaticConfig)', () => { `); }); - test('configure() on test level affects all tests', async () => { + test("configure() on test level affects all tests", async () => { await runFixture({ include, provide: { - configureScope: 'test', - configureOptions: { pauseAnimationAtEnd: true, prefersReducedMotion: 'reduce' }, + configureScope: "test", + configureOptions: { pauseAnimationAtEnd: true, prefersReducedMotion: "reduce" }, }, }); @@ -212,12 +213,12 @@ describe('configure(ChromaticConfig)', () => { `); }); - test('configure() on a single test', async () => { + test("configure() on a single test", async () => { await runFixture({ include, provide: { - configureScope: 'test-second', - configureOptions: { diffIncludeAntiAliasing: true, forcedColors: 'active' }, + configureScope: "test-second", + configureOptions: { diffIncludeAntiAliasing: true, forcedColors: "active" }, }, }); @@ -235,16 +236,16 @@ describe('configure(ChromaticConfig)', () => { `); }); - test('configured options override the global plugin options', async () => { + test("configured options override the global plugin options", async () => { await runFixture( { include, provide: { - configureScope: 'test-second', - configureOptions: { delay: 300, forcedColors: 'changed value here' }, + configureScope: "test-second", + configureOptions: { delay: 300, forcedColors: "changed value here" }, }, }, - { delay: 100, diffThreshold: 0.1, forcedColors: 'active' } + { delay: 100, diffThreshold: 0.1, forcedColors: "active" }, ); // Test #2 should have overriden options @@ -279,13 +280,13 @@ describe('configure(ChromaticConfig)', () => { `); }); - test('disableAutoSnapshot and assetDomains are not written to the result', async () => { + test("disableAutoSnapshot and assetDomains are not written to the result", async () => { await runFixture( { include, - provide: { configureScope: 'module', configureOptions: { disableAutoSnapshot: false } }, + provide: { configureScope: "module", configureOptions: { disableAutoSnapshot: false } }, }, - { assetDomains: ['www.example.com'] } + { assetDomains: ["www.example.com"] }, ); expect(getChromaticOptions()).toMatchInlineSnapshot(` @@ -299,12 +300,12 @@ describe('configure(ChromaticConfig)', () => { `); }); - test('configure({ title })', async () => { + test("configure({ title })", async () => { await runFixture({ include, provide: { - configureScope: 'module', - configureOptions: { title: 'Components/Accordion' }, + configureScope: "module", + configureOptions: { title: "Components/Accordion" }, }, }); @@ -326,19 +327,19 @@ describe('configure(ChromaticConfig)', () => { function getSnapshottedTests() { return vi.mocked(shared.writeTestResult).mock.calls.map((call) => { - return Object.keys(call[1])[0].split(' / ')[0]; + return Object.keys(call[1])[0].split(" / ")[0]; }); } function getChromaticOptions() { return Object.fromEntries( vi.mocked(shared.writeTestResult).mock.calls.map((call) => { - const title = Object.keys(call[1])[0].split(' / ')[0]; + const title = Object.keys(call[1])[0].split(" / ")[0]; // Options are written to file system as JSON, simulate that in mocked writeTestResult stub: const options = JSON.parse(JSON.stringify(call[3])); return [title, options]; - }) + }), ); } diff --git a/packages/vitest/src/browser/public/configure.ts b/packages/vitest/src/browser/public/configure.ts index a62e9885..f72e7e15 100644 --- a/packages/vitest/src/browser/public/configure.ts +++ b/packages/vitest/src/browser/public/configure.ts @@ -1,8 +1,9 @@ -import { beforeAll } from 'vitest'; -import { getCurrentSuite, getCurrentTest, type Test } from '../getCurrentTest'; -import { isChromium } from '../isChromium'; -import { trackEvent } from '../telemetry'; -import type { ConfigureOptions } from '../../types'; +import { beforeAll } from "vitest"; + +import type { ConfigureOptions } from "../../types"; +import { getCurrentSuite, getCurrentTest, type Test } from "../getCurrentTest"; +import { isChromium } from "../isChromium"; +import { trackEvent } from "../telemetry"; /** * Configure options for the current scope. @@ -77,10 +78,10 @@ export function configure(options: ConfigureOptions) { ...options, }; - return trackConfigure('test'); + return trackConfigure("test"); } - const scope = getCurrentSuite()?.suite ? 'suite' : 'file'; + const scope = getCurrentSuite()?.suite ? "suite" : "file"; // Called at top level or within describe(). // Wrap suite traversal in beforeAll to make sure it runs after test collection. @@ -91,7 +92,7 @@ export function configure(options: ConfigureOptions) { traverseTests(suite); function traverseTests(task: (typeof suite.tasks)[0]) { - if (task.type === 'test') { + if (task.type === "test") { const test = task as Test; test.meta.__chromatic_options = { @@ -106,10 +107,10 @@ export function configure(options: ConfigureOptions) { } }); - function trackConfigure(scope: 'test' | 'suite' | 'file') { + function trackConfigure(scope: "test" | "suite" | "file") { trackEvent({ - eventType: 'configure_called', - level: 'info', + eventType: "configure_called", + level: "info", payload: { options: Object.keys(options) as (keyof typeof options)[], scope, diff --git a/packages/vitest/src/browser/public/takeSnapshot.browser.test.ts b/packages/vitest/src/browser/public/takeSnapshot.browser.test.ts index b0a22f6d..f64155f3 100644 --- a/packages/vitest/src/browser/public/takeSnapshot.browser.test.ts +++ b/packages/vitest/src/browser/public/takeSnapshot.browser.test.ts @@ -1,21 +1,22 @@ -import { beforeEach, describe, expect, inject, test, vi } from 'vitest'; -import { commands, page } from 'vitest/browser'; -import { takeSnapshot } from './takeSnapshot'; -import { configure } from './configure'; +import { beforeEach, describe, expect, inject, test, vi } from "vitest"; +import { commands, page } from "vitest/browser"; + +import { configure } from "./configure"; +import { takeSnapshot } from "./takeSnapshot"; beforeEach(() => { - document.body.innerHTML = ''; + document.body.innerHTML = ""; }); -test('saves snapshot on server', async ({ task }) => { - const h1 = document.createElement('h1'); - h1.textContent = 'Example heading'; +test("saves snapshot on server", async ({ task }) => { + const h1 = document.createElement("h1"); + h1.textContent = "Example heading"; document.body.appendChild(h1); - await takeSnapshot('example'); + await takeSnapshot("example"); const snapshots = await commands.__chromatic_getSnapshots(task.id); - expect(snapshots).toHaveProperty('example'); + expect(snapshots).toHaveProperty("example"); expect(snapshots.example.snapshot).toMatchInlineSnapshot(` { @@ -34,22 +35,22 @@ test('saves snapshot on server', async ({ task }) => { `); }); -test('saves pseudo class ids on server', async ({ task }) => { - const hovered = document.createElement('button'); - hovered.textContent = 'Hover here'; +test("saves pseudo class ids on server", async ({ task }) => { + const hovered = document.createElement("button"); + hovered.textContent = "Hover here"; document.body.appendChild(hovered); - const focused = document.createElement('button'); - focused.textContent = 'Focus here'; + const focused = document.createElement("button"); + focused.textContent = "Focus here"; document.body.appendChild(focused); - await page.getByRole('button', { name: 'Focus here' }).click(); - await page.getByRole('button', { name: 'Hover here' }).hover(); + await page.getByRole("button", { name: "Focus here" }).click(); + await page.getByRole("button", { name: "Hover here" }).hover(); - await takeSnapshot('example'); + await takeSnapshot("example"); const snapshots = await commands.__chromatic_getSnapshots(task.id); - expect(snapshots).toHaveProperty('example'); + expect(snapshots).toHaveProperty("example"); expect(snapshots.example.pseudoClassIds).toMatchInlineSnapshot(` { @@ -67,23 +68,23 @@ test('saves pseudo class ids on server', async ({ task }) => { `); }); -test('saves multiple snapshots', async ({ task }) => { - await takeSnapshot('example-1'); - await takeSnapshot('example-2'); +test("saves multiple snapshots", async ({ task }) => { + await takeSnapshot("example-1"); + await takeSnapshot("example-2"); const snapshots = await commands.__chromatic_getSnapshots(task.id); - expect(snapshots).toHaveProperty('example-1'); - expect(snapshots).toHaveProperty('example-2'); + expect(snapshots).toHaveProperty("example-1"); + expect(snapshots).toHaveProperty("example-2"); }); -test('implicit snapshot names increment', async ({ task }) => { +test("implicit snapshot names increment", async ({ task }) => { await takeSnapshot(); { const snapshots = await commands.__chromatic_getSnapshots(task.id); - expect(snapshots).toHaveProperty('Snapshot #1'); + expect(snapshots).toHaveProperty("Snapshot #1"); expect(Object.keys(snapshots)).toHaveLength(1); } @@ -92,27 +93,27 @@ test('implicit snapshot names increment', async ({ task }) => { { const snapshots = await commands.__chromatic_getSnapshots(task.id); - expect.soft(snapshots).toHaveProperty('Snapshot #1'); - expect.soft(snapshots).toHaveProperty('Snapshot #2'); + expect.soft(snapshots).toHaveProperty("Snapshot #1"); + expect.soft(snapshots).toHaveProperty("Snapshot #2"); expect.soft(Object.keys(snapshots)).toHaveLength(2); } }); -test('blob URLs are replaced with data URLs', async ({ task }) => { +test("blob URLs are replaced with data URLs", async ({ task }) => { const blob = await fetch( - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAASCAQAAADIvofAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfhBhAPKSstM+EuAAAAvUlEQVQY05WQIW4CYRgF599gEZgeoAKBWIfCNSmVvQMe3wv0ChhIViKwtTQEAYJwhgpISBA0JSxNIdlB7LIGTJ/8kpeZ7wW5TcT9o/QNBtvOrrWMrtg0sSGOFeELbHlCDsQ+ukeYiHNFJPHBDRKlQKVEbFkLUT3AiAxI6VGCXsWXAoQLBUl5E7HjUFwiyI4zf/wWoB3CFnxX5IeGdY8IGU/iwE9jcZrLy4pnEat+FL4hf/cbqREKo/Cf6W5zASVMeh234UtGAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTA2LTE2VDE1OjQxOjQzLTA3OjAwd1xNIQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNy0wNi0xNlQxNTo0MTo0My0wNzowMAYB9Z0AAAAASUVORK5CYII=' + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAASCAQAAADIvofAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfhBhAPKSstM+EuAAAAvUlEQVQY05WQIW4CYRgF599gEZgeoAKBWIfCNSmVvQMe3wv0ChhIViKwtTQEAYJwhgpISBA0JSxNIdlB7LIGTJ/8kpeZ7wW5TcT9o/QNBtvOrrWMrtg0sSGOFeELbHlCDsQ+ukeYiHNFJPHBDRKlQKVEbFkLUT3AiAxI6VGCXsWXAoQLBUl5E7HjUFwiyI4zf/wWoB3CFnxX5IeGdY8IGU/iwE9jcZrLy4pnEat+FL4hf/cbqREKo/Cf6W5zASVMeh234UtGAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTA2LTE2VDE1OjQxOjQzLTA3OjAwd1xNIQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNy0wNi0xNlQxNTo0MTo0My0wNzowMAYB9Z0AAAAASUVORK5CYII=", ).then((res) => res.blob()); - const img = document.createElement('img'); + const img = document.createElement("img"); const loaded = new Promise((resolve) => (img.onload = resolve)); img.src = window.URL.createObjectURL(blob); document.body.appendChild(img); await loaded; - await takeSnapshot('example'); + await takeSnapshot("example"); const snapshots = await commands.__chromatic_getSnapshots(task.id); - expect(snapshots).toHaveProperty('example'); + expect(snapshots).toHaveProperty("example"); const { snapshot } = snapshots.example; expect(snapshot).toMatchInlineSnapshot(` @@ -131,14 +132,14 @@ test('blob URLs are replaced with data URLs', async ({ task }) => { expect(img.src).toMatch(/^blob:/); }); -describe('snapshot failure tracking', () => { +describe("snapshot failure tracking", () => { configure({ disableAutoSnapshot: true }); beforeEach(() => { - const options = inject('__chromatic_options'); + const options = inject("__chromatic_options"); options.telemetry.enabled = true; - const telemetry = vi.spyOn(commands, '__chromatic_telemetry').mockResolvedValue(undefined); + const telemetry = vi.spyOn(commands, "__chromatic_telemetry").mockResolvedValue(undefined); return () => { options.telemetry.enabled = false; @@ -146,59 +147,59 @@ describe('snapshot failure tracking', () => { }; }); - test('DOM snapshot capture fails', async () => { - document.body.innerHTML = ''; + test("DOM snapshot capture fails", async () => { + document.body.innerHTML = ""; - vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockThrowOnce( - new Error('Simulated capture failure') + vi.spyOn(HTMLCanvasElement.prototype, "toDataURL").mockThrowOnce( + new Error("Simulated capture failure"), ); - await expect(takeSnapshot('example')).rejects.toThrow('Simulated capture failure'); + await expect(takeSnapshot("example")).rejects.toThrow("Simulated capture failure"); expect(commands.__chromatic_telemetry).toHaveBeenCalledWith({ - eventType: 'snapshot_error', - level: 'error', + eventType: "snapshot_error", + level: "error", payload: { - operation: 'capture', + operation: "capture", isAutomaticSnapshot: false, - error: expect.stringContaining('Simulated capture failure'), + error: expect.stringContaining("Simulated capture failure"), }, }); }); - test('tracks snapshot_error when blob URL replacement fails', async () => { - const revokedURL = URL.createObjectURL(new Blob([''], { type: 'image/png' })); + test("tracks snapshot_error when blob URL replacement fails", async () => { + const revokedURL = URL.createObjectURL(new Blob([""], { type: "image/png" })); URL.revokeObjectURL(revokedURL); document.body.innerHTML = ``; - await expect(takeSnapshot('example')).rejects.toThrow('Failed to fetch'); + await expect(takeSnapshot("example")).rejects.toThrow("Failed to fetch"); expect(commands.__chromatic_telemetry).toHaveBeenCalledWith({ - eventType: 'snapshot_error', - level: 'error', + eventType: "snapshot_error", + level: "error", payload: { - operation: 'replace-blob-urls', + operation: "replace-blob-urls", isAutomaticSnapshot: false, - error: expect.stringContaining('Failed to fetch'), + error: expect.stringContaining("Failed to fetch"), }, }); }); - test('tracks snapshot_error when DOM snapshot upload fails', async () => { - vi.spyOn(commands, '__chromatic_uploadDOMSnapshot').mockRejectedValueOnce( - new Error('Simulated upload failure') + test("tracks snapshot_error when DOM snapshot upload fails", async () => { + vi.spyOn(commands, "__chromatic_uploadDOMSnapshot").mockRejectedValueOnce( + new Error("Simulated upload failure"), ); - await expect(takeSnapshot('example')).rejects.toThrow('Simulated upload failure'); + await expect(takeSnapshot("example")).rejects.toThrow("Simulated upload failure"); expect(commands.__chromatic_telemetry).toHaveBeenCalledWith({ - eventType: 'snapshot_error', - level: 'error', + eventType: "snapshot_error", + level: "error", payload: { - operation: 'upload', + operation: "upload", isAutomaticSnapshot: false, - error: expect.stringContaining('Simulated upload failure'), + error: expect.stringContaining("Simulated upload failure"), }, }); }); diff --git a/packages/vitest/src/browser/public/takeSnapshot.test.ts b/packages/vitest/src/browser/public/takeSnapshot.test.ts index c3ce4cb9..c5c024eb 100644 --- a/packages/vitest/src/browser/public/takeSnapshot.test.ts +++ b/packages/vitest/src/browser/public/takeSnapshot.test.ts @@ -1,25 +1,26 @@ -import { expect, test, vi } from 'vitest'; -import * as shared from '@chromatic-com/shared-e2e'; -import { getBrowserConfig, isVitest5, runFixture } from '../../../test/utils/node'; +import * as shared from "@chromatic-com/shared-e2e"; +import { expect, test, vi } from "vitest"; -vi.mock('@chromatic-com/shared-e2e'); +import { getBrowserConfig, isVitest5, runFixture } from "../../../test/utils/node"; + +vi.mock("@chromatic-com/shared-e2e"); vi.mocked(shared.writeTestResult).mockImplementation(() => - Promise.resolve({ storiesFile: 'test.stories.json' }) + Promise.resolve({ storiesFile: "test.stories.json" }), ); /** See {@link file://./../../../test/fixtures/take-snapshot.test.ts} */ -const takeSnapshotTest = 'take-snapshot.test.ts'; +const takeSnapshotTest = "take-snapshot.test.ts"; /** See {@link file://./../../../test/fixtures/viewports.test.ts} */ -const viewportsTest = 'viewports.test.ts'; +const viewportsTest = "viewports.test.ts"; -test('provides descriptive error when called in non-registered test', async () => { +test("provides descriptive error when called in non-registered test", async () => { const { stderr } = await runFixture( { include: [takeSnapshotTest], - provide: { testName: 'one' }, + provide: { testName: "one" }, }, - { disabled: true } + { disabled: true }, ); if (isVitest5()) { @@ -30,13 +31,13 @@ test('provides descriptive error when called in non-registered test', async () = FAIL chromium take-snapshot.test.ts > test #1 TypeError: takeSnapshot() cannot be called in a test that is not registered for Chromatic plugin. Make sure chromium project has chromaticPlugin() enabled. - ❯ take-snapshot.test.ts:7:9 - 5| document.body.innerHTML = '

Example heading

'; - 6| - 7| await takeSnapshot(); + ❯ take-snapshot.test.ts:8:9 + 6| document.body.innerHTML = "

Example heading

"; + 7| + 8| await takeSnapshot(); | ^ - 8| - 9| expect.fail('Should not reach this point'); + 9| + 10| expect.fail("Should not reach this point"); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯" `); @@ -48,23 +49,23 @@ test('provides descriptive error when called in non-registered test', async () = FAIL chromium take-snapshot.test.ts > test #1 TypeError: takeSnapshot() cannot be called in a test that is not registered for Chromatic plugin. Make sure chromium project has chromaticPlugin() enabled. - ❯ take-snapshot.test.ts:7:8 - 5| document.body.innerHTML = '

Example heading

'; - 6| - 7| await takeSnapshot(); + ❯ take-snapshot.test.ts:8:8 + 6| document.body.innerHTML = "

Example heading

"; + 7| + 8| await takeSnapshot(); | ^ - 8| - 9| expect.fail('Should not reach this point'); + 9| + 10| expect.fail("Should not reach this point"); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯" `); } }); -test('provides descriptive error when called outside of a test()', async () => { +test("provides descriptive error when called outside of a test()", async () => { const { stderr } = await runFixture({ include: [takeSnapshotTest], - provide: { testName: 'two' }, + provide: { testName: "two" }, }); if (isVitest5()) { @@ -74,13 +75,13 @@ test('provides descriptive error when called outside of a test()', async () => { FAIL chromium take-snapshot.test.ts > suite TypeError: takeSnapshot() must be called within a test() - ❯ take-snapshot.test.ts:14:11 - 12| describe.runIf(inject('testName') === 'two')('suite', async () => { - 13| beforeAll(async () => { - 14| await takeSnapshot(); + ❯ take-snapshot.test.ts:15:11 + 13| describe.runIf(inject("testName") === "two")("suite", async () => { + 14| beforeAll(async () => { + 15| await takeSnapshot(); | ^ - 15| }); - 16| + 16| }); + 17| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯" `); @@ -91,23 +92,23 @@ test('provides descriptive error when called outside of a test()', async () => { FAIL chromium take-snapshot.test.ts > suite TypeError: takeSnapshot() must be called within a test() - ❯ take-snapshot.test.ts:14:10 - 12| describe.runIf(inject('testName') === 'two')('suite', async () => { - 13| beforeAll(async () => { - 14| await takeSnapshot(); + ❯ take-snapshot.test.ts:15:10 + 13| describe.runIf(inject("testName") === "two")("suite", async () => { + 14| beforeAll(async () => { + 15| await takeSnapshot(); | ^ - 15| }); - 16| + 16| }); + 17| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯" `); } }); -test('provides descriptive error when not awaited', async () => { +test("provides descriptive error when not awaited", async () => { const { stderr } = await runFixture({ include: [takeSnapshotTest], - provide: { testName: 'three' }, + provide: { testName: "three" }, }); if (isVitest5()) { @@ -117,25 +118,25 @@ test('provides descriptive error when not awaited', async () => { FAIL chromium take-snapshot.test.ts > test #3 Error: takeSnapshot() call was not awaited! - ❯ take-snapshot.test.ts:23:3 - 21| document.body.innerHTML = '

Example heading

'; - 22| - 23| takeSnapshot(); // Leave the promise floating, no await + ❯ take-snapshot.test.ts:24:3 + 22| document.body.innerHTML = "

Example heading

"; + 23| + 24| takeSnapshot(); // Leave the promise floating, no await | ^ - 24| - 25| // another + 25| + 26| // another ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ FAIL chromium take-snapshot.test.ts > test #3 Error: takeSnapshot() call was not awaited! - ❯ take-snapshot.test.ts:26:3 - 24| - 25| // another - 26| takeSnapshot(); + ❯ take-snapshot.test.ts:27:3 + 25| + 26| // another + 27| takeSnapshot(); | ^ - 27| }); - 28| + 28| }); + 29| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯" `); @@ -146,32 +147,32 @@ test('provides descriptive error when not awaited', async () => { FAIL chromium take-snapshot.test.ts > test #3 Error: takeSnapshot() call was not awaited! - ❯ take-snapshot.test.ts:23:2 - 21| document.body.innerHTML = '

Example heading

'; - 22| - 23| takeSnapshot(); // Leave the promise floating, no await + ❯ take-snapshot.test.ts:24:2 + 22| document.body.innerHTML = "

Example heading

"; + 23| + 24| takeSnapshot(); // Leave the promise floating, no await | ^ - 24| - 25| // another + 25| + 26| // another ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ FAIL chromium take-snapshot.test.ts > test #3 Error: takeSnapshot() call was not awaited! - ❯ take-snapshot.test.ts:26:2 - 24| - 25| // another - 26| takeSnapshot(); + ❯ take-snapshot.test.ts:27:2 + 25| + 26| // another + 27| takeSnapshot(); | ^ - 27| }); - 28| + 28| }); + 29| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯" `); } }); -test('viewports are correct when --browser.ui=true', async () => { +test("viewports are correct when --browser.ui=true", async () => { await runFixture({ include: [viewportsTest], browser: { ...getBrowserConfig(), ui: true, viewport: { width: 1280, height: 720 } }, @@ -189,7 +190,7 @@ test('viewports are correct when --browser.ui=true', async () => { `); }); -test('viewports are correct when --browser.ui=false', async () => { +test("viewports are correct when --browser.ui=false", async () => { await runFixture({ include: [viewportsTest], browser: { ...getBrowserConfig(), ui: false, viewport: { width: 1280, height: 720 } }, @@ -207,22 +208,22 @@ test('viewports are correct when --browser.ui=false', async () => { `); }); -test.each(['list', 'stack'] as const)( +test.each(["list", "stack"] as const)( "autosnapshot is taken before user registered afterEach runs when {sequence.hooks: '%s'}", async (hooks) => { await runFixture({ include: [takeSnapshotTest], - provide: { testName: 'four' }, - setupFiles: ['custom-setup-file.ts'], + provide: { testName: "four" }, + setupFiles: ["custom-setup-file.ts"], sequence: { hooks }, }); expect(shared.writeTestResult).toHaveBeenCalledTimes(1); const [, snapshots] = vi.mocked(shared.writeTestResult).mock.calls[0]; - expect(snapshots).toHaveProperty('test #4 / Snapshot #1'); + expect(snapshots).toHaveProperty("test #4 / Snapshot #1"); - const { snapshot } = snapshots['test #4 / Snapshot #1']; + const { snapshot } = snapshots["test #4 / Snapshot #1"]; expect(JSON.parse(snapshot.toString())).toMatchInlineSnapshot(` { @@ -239,39 +240,39 @@ test.each(['list', 'stack'] as const)( "type": 2, } `); - } + }, ); test("warns when {sequence.hooks: 'parallel'} is used", async () => { const { stderr } = await runFixture({ include: [takeSnapshotTest], - provide: { testName: 'four' }, - setupFiles: ['custom-setup-file.ts'], - sequence: { hooks: 'parallel' }, + provide: { testName: "four" }, + setupFiles: ["custom-setup-file.ts"], + sequence: { hooks: "parallel" }, }); expect(shared.writeTestResult).toHaveBeenCalledTimes(1); expect(stderr).toMatchInlineSnapshot( - `" chromatic Using { sequence.hooks: 'parallel' } may cause unstable snapshots. Please set 'sequence.hooks' to 'list' or 'stack' to ensure reliable snapshot ordering."` + `" chromatic Using { sequence.hooks: 'parallel' } may cause unstable snapshots. Please set 'sequence.hooks' to 'list' or 'stack' to ensure reliable snapshot ordering."`, ); }); -test('user defined afterEach can call takeSnapshot()', async () => { +test("user defined afterEach can call takeSnapshot()", async () => { await runFixture({ include: [takeSnapshotTest], - provide: { testName: 'four' }, - setupFiles: ['takesnapshot-setup-file.ts'], + provide: { testName: "four" }, + setupFiles: ["takesnapshot-setup-file.ts"], }); expect(shared.writeTestResult).toHaveBeenCalledTimes(1); const [, snapshots] = vi.mocked(shared.writeTestResult).mock.calls[0]; - expect.soft(snapshots).toHaveProperty('test #4 / Snapshot #1'); + expect.soft(snapshots).toHaveProperty("test #4 / Snapshot #1"); expect.soft(snapshots).toHaveProperty("test #4 / user's after each snapshot!"); - const autoSnapshot = snapshots['test #4 / Snapshot #1'].snapshot; + const autoSnapshot = snapshots["test #4 / Snapshot #1"].snapshot; const userSnapshot = snapshots["test #4 / user's after each snapshot!"].snapshot; expect(JSON.parse(autoSnapshot.toString())).toMatchInlineSnapshot(` @@ -317,7 +318,7 @@ function getSnapshottedTests() { Object.entries(domSnapshots).map(([name, { viewport }]) => [ name, `width=${viewport.width}, height=${viewport.height}`, - ]) + ]), ); return { ...all, ...snapshots }; diff --git a/packages/vitest/src/browser/public/takeSnapshot.ts b/packages/vitest/src/browser/public/takeSnapshot.ts index 5179f0e8..f99a6e26 100644 --- a/packages/vitest/src/browser/public/takeSnapshot.ts +++ b/packages/vitest/src/browser/public/takeSnapshot.ts @@ -1,12 +1,13 @@ -import { assert } from 'vitest'; -import { commands } from 'vitest/browser'; -import { snapshot as rrwebSnapshot, createMirror } from '@chromaui/rrweb-snapshot'; -import { serializedNodeWithId } from '@rrweb/types'; -import { type DOMSnapshots } from '@chromatic-com/shared-e2e'; -import { getCurrentTest } from '../getCurrentTest'; -import { isChromium } from '../isChromium'; -import { trackEvent } from '../telemetry'; -import type {} from '../../node/commands'; +import { type DOMSnapshots } from "@chromatic-com/shared-e2e"; +import { snapshot as rrwebSnapshot, createMirror } from "@chromaui/rrweb-snapshot"; +import { serializedNodeWithId } from "@rrweb/types"; +import { assert } from "vitest"; +import { commands } from "vitest/browser"; + +import type {} from "../../node/commands"; +import { getCurrentTest } from "../getCurrentTest"; +import { isChromium } from "../isChromium"; +import { trackEvent } from "../telemetry"; interface Options { isAutomaticSnapshot: boolean; @@ -29,40 +30,40 @@ async function takeSnapshot(name?: string, options?: Options): Promise { if (!test) { trackEvent({ - eventType: 'take_snapshot_invalid_call', - level: 'error', + eventType: "take_snapshot_invalid_call", + level: "error", payload: { isInsideTest: false, isRegisteredTest: undefined, isAwaited: undefined }, }); - throw new TypeError('takeSnapshot() must be called within a test()'); + throw new TypeError("takeSnapshot() must be called within a test()"); } if (!test.meta.__chromatic_isRegistered) { trackEvent({ - eventType: 'take_snapshot_invalid_call', - level: 'error', + eventType: "take_snapshot_invalid_call", + level: "error", payload: { isInsideTest: true, isRegisteredTest: false, isAwaited: undefined }, }); throw new TypeError( - 'takeSnapshot() cannot be called in a test that is not registered for Chromatic plugin.' + - `\nMake sure ${test.file.projectName || 'root'} project has chromaticPlugin() enabled.` + "takeSnapshot() cannot be called in a test that is not registered for Chromatic plugin." + + `\nMake sure ${test.file.projectName || "root"} project has chromaticPlugin() enabled.`, ); } const mirror = createMirror(); - const domSnapshot = snapshot({ document, mirror, onError: trackSnapshotError('capture') }); + const domSnapshot = snapshot({ document, mirror, onError: trackSnapshotError("capture") }); - const pseudoClassIds: DOMSnapshots[string]['pseudoClassIds'] = {}; + const pseudoClassIds: DOMSnapshots[string]["pseudoClassIds"] = {}; - for (const className of [':hover', ':focus', ':focus-visible', ':active'] as const) { + for (const className of [":hover", ":focus", ":focus-visible", ":active"] as const) { const elements = document.querySelectorAll(className); const ids = Array.from(elements, (el) => mirror.getId(el)).filter((id) => id !== -1); pseudoClassIds[className] = ids; } const save = async () => { - await replaceBlobUrls(domSnapshot).catch(trackSnapshotError('replace-blob-urls')); + await replaceBlobUrls(domSnapshot).catch(trackSnapshotError("replace-blob-urls")); await commands .__chromatic_uploadDOMSnapshot( @@ -70,9 +71,9 @@ async function takeSnapshot(name?: string, options?: Options): Promise { domSnapshot, pseudoClassIds, name ?? null, // Convert undefined to null to avoid https://github.com/vitest-dev/vitest/issues/10864 - options + options, ) - .catch(trackSnapshotError('upload')); + .catch(trackSnapshotError("upload")); test.meta.__chromatic_isTakeSnapshotCalled = true; }; @@ -86,7 +87,7 @@ async function takeSnapshot(name?: string, options?: Options): Promise { * Provide descriptive error if the user forgets to await the takeSnapshot() call. * See {@link file://./takeSnapshot.test.ts} for examples. */ - const error = new Error('takeSnapshot() call was not awaited!'); + const error = new Error("takeSnapshot() call was not awaited!"); Error.captureStackTrace?.(error, takeSnapshot); const pendingCall = { promise: save(), error }; @@ -105,11 +106,11 @@ async function takeSnapshot(name?: string, options?: Options): Promise { * Report a snapshot failure to telemetry before rethrowing it. The error is serialized * eagerly, as `Error` instances would not survive the RPC to the Node process. */ - function trackSnapshotError(operation: 'capture' | 'replace-blob-urls' | 'upload') { + function trackSnapshotError(operation: "capture" | "replace-blob-urls" | "upload") { return function onError(error: unknown) { trackEvent({ - eventType: 'snapshot_error', - level: 'error', + eventType: "snapshot_error", + level: "error", payload: { operation, isAutomaticSnapshot: options?.isAutomaticSnapshot ?? false, @@ -132,7 +133,7 @@ function snapshot(options: { recordCanvas: true, mirror: options.mirror, }); - assert(domSnapshot, 'Failed to capture DOM snapshot'); + assert(domSnapshot, "Failed to capture DOM snapshot"); return domSnapshot; } catch (error) { @@ -142,26 +143,26 @@ function snapshot(options: { } async function replaceBlobUrls(node: serializedNodeWithId) { - if (!('childNodes' in node)) { + if (!("childNodes" in node)) { return; } await Promise.all( node.childNodes.map(async (childNode) => { if ( - 'tagName' in childNode && - childNode.tagName === 'img' && - typeof childNode.attributes.src === 'string' && - childNode.attributes.src?.startsWith('blob:') + "tagName" in childNode && + childNode.tagName === "img" && + typeof childNode.attributes.src === "string" && + childNode.attributes.src?.startsWith("blob:") ) { const base64Url = await toDataURL(childNode.attributes.src); childNode.attributes.src = base64Url; } - if ('childNodes' in childNode && childNode.childNodes?.length) { + if ("childNodes" in childNode && childNode.childNodes?.length) { await replaceBlobUrls(childNode); } - }) + }), ); } @@ -170,7 +171,8 @@ async function toDataURL(url: string): Promise { return new Promise((resolveFileRead, reject) => { const reader = new FileReader(); - reader.onloadend = () => resolveFileRead(reader.result?.toString() || ''); + // oxlint-disable-next-line typescript/no-base-to-string + reader.onloadend = () => resolveFileRead(reader.result?.toString() || ""); reader.onerror = reject; // convert the blob to base64 string diff --git a/packages/vitest/src/browser/public/waitForIdleNetwork.browser.test.ts b/packages/vitest/src/browser/public/waitForIdleNetwork.browser.test.ts index 68bc0624..55ac588d 100644 --- a/packages/vitest/src/browser/public/waitForIdleNetwork.browser.test.ts +++ b/packages/vitest/src/browser/public/waitForIdleNetwork.browser.test.ts @@ -1,8 +1,9 @@ -import { beforeEach, expect, test } from 'vitest'; -import { http, HttpResponse } from 'msw'; -import { setupWorker } from 'msw/browser'; -import { waitForIdleNetwork } from './waitForIdleNetwork'; -import { configure } from './configure'; +import { http, HttpResponse } from "msw"; +import { setupWorker } from "msw/browser"; +import { beforeEach, expect, test } from "vitest"; + +import { configure } from "./configure"; +import { waitForIdleNetwork } from "./waitForIdleNetwork"; const worker = setupWorker(); configure({ disableAutoSnapshot: true }); @@ -16,18 +17,18 @@ beforeEach(async () => { }; }); -test('resolves when network is idle', async () => { +test("resolves when network is idle", async () => { // initial await waitForIdleNetwork(1000); // after request has resolved - worker.use(http.get('/example', () => HttpResponse.json({ ok: true }))); - await fetch('/example'); + worker.use(http.get("/example", () => HttpResponse.json({ ok: true }))); + await fetch("/example"); await expect(waitForIdleNetwork(1000)).resolves.toBeUndefined(); }); -test('throws when network is not idle', async () => { +test("throws when network is not idle", async () => { /* * Start request that's pending until manually aborting it. * This will block the fetch() request and keep network busy during testing. @@ -35,44 +36,44 @@ test('throws when network is not idle', async () => { const controller = new AbortController(); const onRequest = withResolvers(); - worker.use(http.get('/example', () => new Promise(onRequest.resolve))); + worker.use(http.get("/example", () => new Promise(onRequest.resolve))); - const request = fetch('/example', { signal: controller.signal }).finally(() => { + const request = fetch("/example", { signal: controller.signal }).finally(() => { if (!controller.signal.aborted) { - expect.fail('Request should not complete'); + expect.fail("Request should not complete"); } }); await onRequest.promise; await expect(waitForIdleNetwork(100)).rejects.toThrowErrorMatchingInlineSnapshot( - `[NetworkIdleTimeoutError: Timed out waiting for network to be idle]` + `[NetworkIdleTimeoutError: Timed out waiting for network to be idle]`, ); await expect(waitForIdleNetwork(100)).rejects.toThrowErrorMatchingInlineSnapshot( - `[NetworkIdleTimeoutError: Timed out waiting for network to be idle]` + `[NetworkIdleTimeoutError: Timed out waiting for network to be idle]`, ); - controller.abort('cleanup pending request'); - await request.catch((reason) => expect(reason).toBe('cleanup pending request')); + controller.abort("cleanup pending request"); + await request.catch((reason) => expect(reason).toBe("cleanup pending request")); }); -test('resolves when network becomes idle', async () => { +test("resolves when network becomes idle", async () => { const onRequest = withResolvers(); const onBlocked = withResolvers(); worker.use( - http.get('/example', async () => { + http.get("/example", async () => { onRequest.resolve(); await onBlocked.promise; return HttpResponse.json({ ok: true }); - }) + }), ); - void fetch('/example'); + void fetch("/example"); await onRequest.promise; await expect(waitForIdleNetwork(100)).rejects.toThrowErrorMatchingInlineSnapshot( - `[NetworkIdleTimeoutError: Timed out waiting for network to be idle]` + `[NetworkIdleTimeoutError: Timed out waiting for network to be idle]`, ); onBlocked.resolve(); diff --git a/packages/vitest/src/browser/public/waitForIdleNetwork.test.ts b/packages/vitest/src/browser/public/waitForIdleNetwork.test.ts index a86bee6f..c50949ac 100644 --- a/packages/vitest/src/browser/public/waitForIdleNetwork.test.ts +++ b/packages/vitest/src/browser/public/waitForIdleNetwork.test.ts @@ -1,16 +1,17 @@ -import { expect, test } from 'vitest'; -import { isVitest5, runFixture } from '../../../test/utils/node'; +import { expect, test } from "vitest"; + +import { isVitest5, runFixture } from "../../../test/utils/node"; /** See {@link file://./../../../test/fixtures/wait-for-idle-network.test.ts} */ -const include = ['wait-for-idle-network.test.ts']; +const include = ["wait-for-idle-network.test.ts"]; test("throws when used in a test that isn't registered", async () => { const { stderr } = await runFixture( { include, - provide: { testName: 'one' }, + provide: { testName: "one" }, }, - { disabled: true } + { disabled: true }, ); if (isVitest5()) { @@ -21,13 +22,13 @@ test("throws when used in a test that isn't registered", async () => { FAIL chromium wait-for-idle-network.test.ts > test #1 TypeError: waitForIdleNetwork() cannot be called in a test that is not registered for Chromatic plugin. Make sure chromium project has chromaticPlugin() enabled. - ❯ wait-for-idle-network.test.ts:9:9 - 7| document.body.innerHTML = '

Example heading

'; - 8| - 9| await waitForIdleNetwork(1); + ❯ wait-for-idle-network.test.ts:10:9 + 8| document.body.innerHTML = "

Example heading

"; + 9| + 10| await waitForIdleNetwork(1); | ^ - 10| - 11| expect.fail('Should not reach this point'); + 11| + 12| expect.fail("Should not reach this point"); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯" `); @@ -39,23 +40,23 @@ test("throws when used in a test that isn't registered", async () => { FAIL chromium wait-for-idle-network.test.ts > test #1 TypeError: waitForIdleNetwork() cannot be called in a test that is not registered for Chromatic plugin. Make sure chromium project has chromaticPlugin() enabled. - ❯ wait-for-idle-network.test.ts:9:8 - 7| document.body.innerHTML = '

Example heading

'; - 8| - 9| await waitForIdleNetwork(1); + ❯ wait-for-idle-network.test.ts:10:8 + 8| document.body.innerHTML = "

Example heading

"; + 9| + 10| await waitForIdleNetwork(1); | ^ - 10| - 11| expect.fail('Should not reach this point'); + 11| + 12| expect.fail("Should not reach this point"); ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯" `); } }); -test('throws when used outside of a test()', async () => { +test("throws when used outside of a test()", async () => { const { stderr } = await runFixture({ include, - provide: { testName: 'two' }, + provide: { testName: "two" }, }); if (isVitest5()) { @@ -65,13 +66,13 @@ test('throws when used outside of a test()', async () => { FAIL chromium wait-for-idle-network.test.ts > suite TypeError: waitForIdleNetwork() must be called within a test() - ❯ wait-for-idle-network.test.ts:16:11 - 14| describe.runIf(inject('testName') === 'two')('suite', async () => { - 15| beforeAll(async () => { - 16| await waitForIdleNetwork(1); + ❯ wait-for-idle-network.test.ts:17:11 + 15| describe.runIf(inject("testName") === "two")("suite", async () => { + 16| beforeAll(async () => { + 17| await waitForIdleNetwork(1); | ^ - 17| }); - 18| + 18| }); + 19| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯" `); @@ -82,13 +83,13 @@ test('throws when used outside of a test()', async () => { FAIL chromium wait-for-idle-network.test.ts > suite TypeError: waitForIdleNetwork() must be called within a test() - ❯ wait-for-idle-network.test.ts:16:10 - 14| describe.runIf(inject('testName') === 'two')('suite', async () => { - 15| beforeAll(async () => { - 16| await waitForIdleNetwork(1); + ❯ wait-for-idle-network.test.ts:17:10 + 15| describe.runIf(inject("testName") === "two")("suite", async () => { + 16| beforeAll(async () => { + 17| await waitForIdleNetwork(1); | ^ - 17| }); - 18| + 18| }); + 19| ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯" `); diff --git a/packages/vitest/src/browser/public/waitForIdleNetwork.ts b/packages/vitest/src/browser/public/waitForIdleNetwork.ts index eb56c8f6..108c7cd2 100644 --- a/packages/vitest/src/browser/public/waitForIdleNetwork.ts +++ b/packages/vitest/src/browser/public/waitForIdleNetwork.ts @@ -1,8 +1,9 @@ -import { commands } from 'vitest/browser'; -import { getCurrentTest } from '../getCurrentTest'; -import { isChromium } from '../isChromium'; -import { trackEvent } from '../telemetry'; -import type {} from '../../node/commands'; +import { commands } from "vitest/browser"; + +import type {} from "../../node/commands"; +import { getCurrentTest } from "../getCurrentTest"; +import { isChromium } from "../isChromium"; +import { trackEvent } from "../telemetry"; interface Options { _internal: boolean; @@ -36,8 +37,8 @@ async function waitForIdleNetwork(timeout: number, options?: Options): Promise - Promise.resolve({ storiesFile: 'test.stories.json' }) + Promise.resolve({ storiesFile: "test.stories.json" }), ); -test('by default all tests are snapshotted', async () => { +test("by default all tests are snapshotted", async () => { await runFixture({ include, tags }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` @@ -32,14 +33,14 @@ test('by default all tests are snapshotted', async () => { `); }); -test('plugin level disableAutoSnapshot', async () => { +test("plugin level disableAutoSnapshot", async () => { await runFixture({ include, tags }, { disableAutoSnapshot: true }); expect(getSnapshottedTests()).toMatchInlineSnapshot(`[]`); }); -test('plugin level options', async () => { - await runFixture({ include, tags }, { delay: 1234, forcedColors: 'hover', diffThreshold: 0.7 }); +test("plugin level options", async () => { + await runFixture({ include, tags }, { delay: 1234, forcedColors: "hover", diffThreshold: 0.7 }); expect(getChromaticOptions()).toMatchInlineSnapshot(` { @@ -85,10 +86,10 @@ test("does not require user to define Vitest's tags", async () => { `); }); -test('works even if user defines their own custom Vitest tags', async () => { +test("works even if user defines their own custom Vitest tags", async () => { await runFixture( - { include, tags: [{ name: 'custom-tag', description: 'custom desc' }] }, - { tags: tags.map((tag) => tag.name) } + { include, tags: [{ name: "custom-tag", description: "custom desc" }] }, + { tags: tags.map((tag) => tag.name) }, ); expect(getSnapshottedTests()).toMatchInlineSnapshot(` @@ -102,13 +103,13 @@ test('works even if user defines their own custom Vitest tags', async () => { }); test("no tests are snapshotted when tag doesn't match any", async () => { - await runFixture({ include, tags }, { tags: ['example-5'] }); + await runFixture({ include, tags }, { tags: ["example-5"] }); expect(getSnapshottedTests()).toMatchInlineSnapshot(`[]`); }); -test('snapshots tests matching a single tag', async () => { - await runFixture({ include, tags }, { tags: ['example-1'] }); +test("snapshots tests matching a single tag", async () => { + await runFixture({ include, tags }, { tags: ["example-1"] }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` [ @@ -117,8 +118,8 @@ test('snapshots tests matching a single tag', async () => { `); }); -test('snapshots tests matching multiple tags', async () => { - await runFixture({ include, tags }, { tags: ['example-1', 'example-2'] }); +test("snapshots tests matching multiple tags", async () => { + await runFixture({ include, tags }, { tags: ["example-1", "example-2"] }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` [ @@ -128,8 +129,8 @@ test('snapshots tests matching multiple tags', async () => { `); }); -test('snapshots tests matching tag of describe()', async () => { - await runFixture({ include, tags }, { tags: ['example-3'] }); +test("snapshots tests matching tag of describe()", async () => { + await runFixture({ include, tags }, { tags: ["example-3"] }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` [ @@ -139,8 +140,8 @@ test('snapshots tests matching tag of describe()', async () => { `); }); -test('snapshots tests matching tag of nested describe()', async () => { - await runFixture({ include, tags }, { tags: ['example-4'] }); +test("snapshots tests matching tag of nested describe()", async () => { + await runFixture({ include, tags }, { tags: ["example-4"] }); expect(getSnapshottedTests()).toMatchInlineSnapshot(` [ @@ -149,7 +150,7 @@ test('snapshots tests matching tag of nested describe()', async () => { `); }); -test('cleans up internal test meta properties', async () => { +test("cleans up internal test meta properties", async () => { const testModules: TestModule[] = []; await runFixture( @@ -158,7 +159,7 @@ test('cleans up internal test meta properties', async () => { tags, reporters: [{ onTestRunEnd: (results) => void testModules.push(...results) }], }, - { tags: ['example-1', 'example-2'] } + { tags: ["example-1", "example-2"] }, ); const tests = testModules.flatMap((testModule) => Array.from(testModule.children.allTests())); @@ -177,19 +178,19 @@ test('cleans up internal test meta properties', async () => { function getSnapshottedTests() { return vi.mocked(shared.writeTestResult).mock.calls.map((call) => { - return Object.keys(call[1])[0].split(' / ')[0]; + return Object.keys(call[1])[0].split(" / ")[0]; }); } function getChromaticOptions() { return Object.fromEntries( vi.mocked(shared.writeTestResult).mock.calls.map((call) => { - const title = Object.keys(call[1])[0].split(' / ')[0]; + const title = Object.keys(call[1])[0].split(" / ")[0]; // Options are written to file system as JSON, simulate that in mocked writeTestResult stub: const options = JSON.parse(JSON.stringify(call[3])); return [title, options]; - }) + }), ); } diff --git a/packages/vitest/src/browser/setupFile.ts b/packages/vitest/src/browser/setupFile.ts index d5b7be80..977fea6b 100644 --- a/packages/vitest/src/browser/setupFile.ts +++ b/packages/vitest/src/browser/setupFile.ts @@ -1,15 +1,16 @@ -import { afterEach, beforeEach, inject } from 'vitest'; -import { commands } from 'vitest/browser'; -import { takeSnapshot } from './public/takeSnapshot'; -import { waitForIdleNetwork } from './public/waitForIdleNetwork'; -import { trackEvent } from './telemetry'; -import { type InternalTestContext } from '../types'; -import type {} from '../node/commands'; -import type {} from '../node/plugin'; +import { afterEach, beforeEach, inject } from "vitest"; +import { commands } from "vitest/browser"; + +import type {} from "../node/commands"; +import type {} from "../node/plugin"; +import { type InternalTestContext } from "../types"; +import { takeSnapshot } from "./public/takeSnapshot"; +import { waitForIdleNetwork } from "./public/waitForIdleNetwork"; +import { trackEvent } from "./telemetry"; // Destructuring the context is important, so that possible user-provided fixtures work too beforeEach(async ({ task }) => { - const options = inject('__chromatic_options'); + const options = inject("__chromatic_options"); if (options.tags) { // If user has defined plugin tags we register only the tests that match those tags @@ -104,13 +105,13 @@ class PendingSnapshotsError extends AggregateError { constructor(public pendingCalls: Array<{ error: Error }>) { super( pendingCalls.map((call) => call.error), - `${pendingCalls.length} unawaited takeSnapshot() call(s)` + `${pendingCalls.length} unawaited takeSnapshot() call(s)`, ); - this.name = 'PendingSnapshotsError'; + this.name = "PendingSnapshotsError"; trackEvent({ - eventType: 'take_snapshot_invalid_call', - level: 'error', + eventType: "take_snapshot_invalid_call", + level: "error", payload: { isAwaited: false, isInsideTest: true, diff --git a/packages/vitest/src/browser/telemetry.ts b/packages/vitest/src/browser/telemetry.ts index b78dcea4..ea4573af 100644 --- a/packages/vitest/src/browser/telemetry.ts +++ b/packages/vitest/src/browser/telemetry.ts @@ -1,7 +1,8 @@ -import { commands } from 'vitest/browser'; -import { inject } from 'vitest'; -import type {} from '../node/commands'; -import type { EventType, TelemetryEvent } from '../node/telemetry/types'; +import { inject } from "vitest"; +import { commands } from "vitest/browser"; + +import type {} from "../node/commands"; +import type { EventType, TelemetryEvent } from "../node/telemetry/types"; /** * Forward a telemetry event to the Node process, where metadata and other automatically @@ -10,7 +11,7 @@ import type { EventType, TelemetryEvent } from '../node/telemetry/types'; * @internal */ export function trackEvent(event: TelemetryEvent): void { - const enabled = inject('__chromatic_options')?.telemetry.enabled ?? false; + const enabled = inject("__chromatic_options")?.telemetry.enabled ?? false; if (enabled) { void commands.__chromatic_telemetry(event).catch(() => {}); diff --git a/packages/vitest/src/constants.ts b/packages/vitest/src/constants.ts index bda3453d..e0747549 100644 --- a/packages/vitest/src/constants.ts +++ b/packages/vitest/src/constants.ts @@ -1,2 +1,2 @@ // Vitest's convention: https://vitest.dev/guide/advanced/reporters.html#storing-artifacts-on-file-system -export const DEFAULT_OUTPUT_DIR = '.vitest/chromatic'; +export const DEFAULT_OUTPUT_DIR = ".vitest/chromatic"; diff --git a/packages/vitest/src/index.ts b/packages/vitest/src/index.ts index d695ec50..97033e0f 100644 --- a/packages/vitest/src/index.ts +++ b/packages/vitest/src/index.ts @@ -1,5 +1,5 @@ /* Entrypoint for browser context */ -export { takeSnapshot } from './browser/public/takeSnapshot'; -export { configure } from './browser/public/configure'; -export { waitForIdleNetwork } from './browser/public/waitForIdleNetwork'; +export { takeSnapshot } from "./browser/public/takeSnapshot"; +export { configure } from "./browser/public/configure"; +export { waitForIdleNetwork } from "./browser/public/waitForIdleNetwork"; diff --git a/packages/vitest/src/node/NetworkIdleTracker.ts b/packages/vitest/src/node/NetworkIdleTracker.ts index 1d4a1160..6c06b5bf 100644 --- a/packages/vitest/src/node/NetworkIdleTracker.ts +++ b/packages/vitest/src/node/NetworkIdleTracker.ts @@ -1,8 +1,8 @@ -import type {} from '@vitest/browser-playwright'; -import type { CDPSession } from 'vitest/node'; +import type {} from "@vitest/browser-playwright"; +import type { CDPSession } from "vitest/node"; class NetworkIdleTimeoutError extends Error { - name = 'NetworkIdleTimeoutError'; + name = "NetworkIdleTimeoutError"; } export class NetworkIdleTracker { @@ -14,7 +14,7 @@ export class NetworkIdleTracker { private constructor( private cdp: CDPSession, - private idleTimeout: number + private idleTimeout: number, ) {} static async create(cdp: CDPSession, idleTimeout: number) { @@ -23,22 +23,22 @@ export class NetworkIdleTracker { } async watch() { - this.cdp.on('Network.requestWillBeSent', this.onRequest); - this.cdp.on('Network.loadingFinished', this.onComplete); - this.cdp.on('Network.loadingFailed', this.onComplete); + this.cdp.on("Network.requestWillBeSent", this.onRequest); + this.cdp.on("Network.loadingFinished", this.onComplete); + this.cdp.on("Network.loadingFailed", this.onComplete); - await this.cdp.send('Network.enable'); + await this.cdp.send("Network.enable"); } async unwatch() { - this.cdp.off('Network.requestWillBeSent', this.onRequest); - this.cdp.off('Network.loadingFinished', this.onComplete); - this.cdp.off('Network.loadingFailed', this.onComplete); + this.cdp.off("Network.requestWillBeSent", this.onRequest); + this.cdp.off("Network.loadingFinished", this.onComplete); + this.cdp.off("Network.loadingFailed", this.onComplete); this.clearTimer(); this.pending.clear(); this.idleResolvers.splice(0).forEach((fn) => fn()); - await this.cdp.send('Network.disable'); + await this.cdp.send("Network.disable"); } waitForIdle(rejectTimeout: number): Promise { @@ -48,8 +48,8 @@ export class NetworkIdleTracker { return new Promise((resolve, reject) => { const timeout = setTimeout( - () => reject(new NetworkIdleTimeoutError('Timed out waiting for network to be idle')), - rejectTimeout + () => reject(new NetworkIdleTimeoutError("Timed out waiting for network to be idle")), + rejectTimeout, ); this.idleResolvers.push(resolve, () => clearTimeout(timeout)); }); diff --git a/packages/vitest/src/node/commands.browser.test.ts b/packages/vitest/src/node/commands.browser.test.ts index df724fb3..461dab60 100644 --- a/packages/vitest/src/node/commands.browser.test.ts +++ b/packages/vitest/src/node/commands.browser.test.ts @@ -1,15 +1,16 @@ -import { test, expect } from 'vitest'; -import { commands } from 'vitest/browser'; -import { InternalTestContext } from '../types'; +import { test, expect } from "vitest"; +import { commands } from "vitest/browser"; -test('setupFile is registered', async (context) => { +import { InternalTestContext } from "../types"; + +test("setupFile is registered", async (context) => { expect(context.task.meta.__chromatic_isRegistered).toBe(true); expect(context.task.meta.__chromatic_isTakeSnapshotCalled).toBe(false); }); -test('browser commands are available', () => { +test("browser commands are available", () => { const chromaticCommands = Object.entries(commands).filter(([name]) => - name.startsWith('__chromatic_') + name.startsWith("__chromatic_"), ); expect(Object.fromEntries(chromaticCommands)).toMatchInlineSnapshot(` diff --git a/packages/vitest/src/node/commands.test.ts b/packages/vitest/src/node/commands.test.ts index c34858e3..04c033a0 100644 --- a/packages/vitest/src/node/commands.test.ts +++ b/packages/vitest/src/node/commands.test.ts @@ -1,23 +1,24 @@ -import { expect, test, vi } from 'vitest'; -import * as shared from '@chromatic-com/shared-e2e'; -import type { BrowserCommandContext } from 'vitest/node'; -import { runFixture } from '../../test/utils/node'; -import { createCommands } from './commands'; -import { trackEvent } from './telemetry'; -import type { ResolvedOptions } from '../types'; - -vi.mock('@chromatic-com/shared-e2e/write-archive/index'); +import * as shared from "@chromatic-com/shared-e2e"; +import { expect, test, vi } from "vitest"; +import type { BrowserCommandContext } from "vitest/node"; + +import { runFixture } from "../../test/utils/node"; +import type { ResolvedOptions } from "../types"; +import { createCommands } from "./commands"; +import { trackEvent } from "./telemetry"; + +vi.mock("@chromatic-com/shared-e2e/write-archive/index"); vi.mocked(shared.writeTestResult).mockImplementation(() => - Promise.resolve({ storiesFile: 'test.stories.json' }) + Promise.resolve({ storiesFile: "test.stories.json" }), ); // Prevent trackEvent from sending real events while keeping call records -vi.mock('./telemetry', { spy: true }); +vi.mock("./telemetry", { spy: true }); vi.mocked(trackEvent).mockImplementation(() => {}); -test('writes test results with full test name', async () => { +test("writes test results with full test name", async () => { /** See {@link file://./../../test/fixtures/test-names.test.ts} */ - await runFixture({ include: ['test-names.test.ts'] }); + await runFixture({ include: ["test-names.test.ts"] }); const tests = vi .mocked(shared.writeTestResult) @@ -69,10 +70,10 @@ test('writes test results with full test name', async () => { `); }); -test('writes test title without extensions', async () => { +test("writes test title without extensions", async () => { /** See {@link file://./../../test/fixtures/nested/directories/expected-name.this-should-be-ignored.and-this.test.ts} */ await runFixture({ - include: ['nested/directories/expected-name.this-should-be-ignored.and-this.test.ts'], + include: ["nested/directories/expected-name.this-should-be-ignored.and-this.test.ts"], }); expect(shared.writeTestResult).toHaveBeenCalledTimes(1); @@ -87,15 +88,15 @@ test('writes test title without extensions', async () => { `); }); -test('writes test results with DOM snapshot', async () => { +test("writes test results with DOM snapshot", async () => { /** See {@link file://./../../test/fixtures/dom.test.ts} */ - await runFixture({ include: ['dom.test.ts'] }); + await runFixture({ include: ["dom.test.ts"] }); const snapshots = vi.mocked(shared.writeTestResult).mock.calls.map((call) => call[1]); - expect(snapshots[0]).toHaveProperty('mount some elements / Snapshot #1'); + expect(snapshots[0]).toHaveProperty("mount some elements / Snapshot #1"); - const { snapshot } = snapshots[0]['mount some elements / Snapshot #1']; + const { snapshot } = snapshots[0]["mount some elements / Snapshot #1"]; const json = JSON.parse(Buffer.from(snapshot).toString()); expect(json).toMatchInlineSnapshot(` @@ -130,9 +131,9 @@ test('writes test results with DOM snapshot', async () => { `); }); -test('writes test results with custom parameters', async () => { +test("writes test results with custom parameters", async () => { /** See {@link file://./../../test/fixtures/take-snapshot.test.ts} */ - await runFixture({ include: ['take-snapshot.test.ts'], provide: { testName: 'five' } }); + await runFixture({ include: ["take-snapshot.test.ts"], provide: { testName: "five" } }); const snapshots = vi.mocked(shared.writeTestResult).mock.calls.map((call) => call[1]); const parameters = snapshots @@ -188,14 +189,14 @@ test('writes test results with custom parameters', async () => { `); }); -test('writes archived assets even from first URL archiver sees', async () => { +test("writes archived assets even from first URL archiver sees", async () => { /** See {@link file://./../../test/fixtures/external-assets.test.ts} */ - await runFixture({ include: ['external-assets.test.ts'] }); + await runFixture({ include: ["external-assets.test.ts"] }); const assets = vi.mocked(shared.writeTestResult).mock.calls[0][2]; const url = Object.keys(assets)[0]; - expect(url, 'Expected to archive assets.external.css').toBeDefined(); + expect(url, "Expected to archive assets.external.css").toBeDefined(); expect(url).toMatch(/assets\/external.css$/); expect(parseArchive(assets[url])).toMatchInlineSnapshot(` @@ -208,20 +209,20 @@ test('writes archived assets even from first URL archiver sees', async () => { `); }); -test('writes archived assets even when browser cached them', { timeout: 30_000 }, async () => { +test("writes archived assets even when browser cached them", { timeout: 30_000 }, async () => { await runFixture({ include: [ /** See {@link file://./../../test/fixtures/external-assets.test.ts} */ - 'external-assets.test.ts', + "external-assets.test.ts", /** See {@link file://./../../test/fixtures/external-assets-caching-1.test.ts} */ - 'external-assets-caching-1.test.ts', - 'external-assets-caching-2.test.ts', - 'external-assets-caching-3.test.ts', - 'external-assets-caching-4.test.ts', - 'external-assets-caching-5.test.ts', + "external-assets-caching-1.test.ts", + "external-assets-caching-2.test.ts", + "external-assets-caching-3.test.ts", + "external-assets-caching-4.test.ts", + "external-assets-caching-5.test.ts", ], - provide: { testName: 'both' }, + provide: { testName: "both" }, // Run 2 browser contexts parallel, 3 test files in each. // This triggers complex caching case where 3 files share same cache, but the other 3 don't. @@ -256,7 +257,7 @@ test('writes archived assets even when browser cached them', { timeout: 30_000 } expect( assets, - `Expected to archive 12 assets, got ${formatResourceArchives(assets)}` + `Expected to archive 12 assets, got ${formatResourceArchives(assets)}`, ).toHaveLength(2 * 6); for (const [index, asset] of assets.entries()) { @@ -266,39 +267,39 @@ test('writes archived assets even when browser cached them', { timeout: 30_000 } expect(url).toMatch(/assets\/external.css$/); expect(parseArchive(asset[url])).toMatchObject({ - contentType: 'text/css', + contentType: "text/css", statusCode: 200, - statusText: 'OK', - body: 'body { background-color: red; }', + statusText: "OK", + body: "body { background-color: red; }", }); } }); -test('tracks command_failed when a command fails', async () => { +test("tracks command_failed when a command fails", async () => { const commands = createCommands({ telemetry: { enabled: true } } as ResolvedOptions); const context = createContext(); await expect(commands.__chromatic_waitForIdleNetwork(context, 1)).rejects.toThrow( - 'No network idle tracker found for session session-1' + "No network idle tracker found for session session-1", ); expect(trackEvent).toHaveBeenCalledWith( { - eventType: 'command_failed', - level: 'error', + eventType: "command_failed", + level: "error", payload: { - command: '__chromatic_waitForIdleNetwork', + command: "__chromatic_waitForIdleNetwork", error: expect.objectContaining({ - message: 'No network idle tracker found for session session-1', + message: "No network idle tracker found for session session-1", }), }, }, context.project.vitest, - expect.anything() + expect.anything(), ); }); -test('identical failures are tracked only once', async () => { +test("identical failures are tracked only once", async () => { const commands = createCommands({ telemetry: { enabled: true } } as ResolvedOptions); const context = createContext(); @@ -308,7 +309,7 @@ test('identical failures are tracked only once', async () => { expect(getCommandFailedEvents()).toHaveLength(1); }); -test('identical failures are tracked again after reset', async () => { +test("identical failures are tracked again after reset", async () => { const commands = createCommands({ telemetry: { enabled: true } } as ResolvedOptions); const context = createContext(); @@ -319,19 +320,19 @@ test('identical failures are tracked again after reset', async () => { expect(getCommandFailedEvents()).toHaveLength(2); }); -test('telemetry command failures are not tracked', async () => { - vi.mocked(trackEvent).mockThrow(new Error('Mock error')); +test("telemetry command failures are not tracked", async () => { + vi.mocked(trackEvent).mockThrow(new Error("Mock error")); const commands = createCommands({ telemetry: { enabled: true } } as ResolvedOptions); const context = createContext(); await expect( commands.__chromatic_telemetry(context, { - eventType: 'run_started', - level: 'info', + eventType: "run_started", + level: "info", payload: {}, - }) - ).rejects.toThrow('Mock error'); + }), + ).rejects.toThrow("Mock error"); expect(getCommandFailedEvents()).toHaveLength(0); }); @@ -340,25 +341,25 @@ function formatResourceArchives(archives: shared.ResourceArchive[]) { return JSON.stringify( archives.map((archive) => Object.keys(archive).map((url) => parseArchive(archive[url]))), null, - 2 + 2, ); } function parseArchive(asset: shared.ArchiveResponse) { - if ('error' in asset) { + if ("error" in asset) { return asset; } return { ...asset, - body: Buffer.from(asset.body).toString().trim().replace(/\s+/g, ' '), + body: Buffer.from(asset.body).toString().trim().replace(/\s+/g, " "), }; } function createContext() { - return { sessionId: 'session-1', project: { vitest: {} } } as unknown as BrowserCommandContext; + return { sessionId: "session-1", project: { vitest: {} } } as unknown as BrowserCommandContext; } function getCommandFailedEvents() { - return vi.mocked(trackEvent).mock.calls.filter(([event]) => event.eventType === 'command_failed'); + return vi.mocked(trackEvent).mock.calls.filter(([event]) => event.eventType === "command_failed"); } diff --git a/packages/vitest/src/node/commands.ts b/packages/vitest/src/node/commands.ts index 7cc1e583..7d0f7eea 100644 --- a/packages/vitest/src/node/commands.ts +++ b/packages/vitest/src/node/commands.ts @@ -1,30 +1,32 @@ -import assert from 'node:assert'; -import { resolve } from 'node:path'; -import { storyNameFromExport, toId } from 'storybook/internal/csf'; +import assert from "node:assert"; +import { resolve } from "node:path"; + +import { ResourceArchiver, writeTestResult, type DOMSnapshots } from "@chromatic-com/shared-e2e"; +import { type serializedNodeWithId } from "@rrweb/types"; +import { type PlaywrightProviderOptions } from "@vitest/browser-playwright"; +import { storyNameFromExport, toId } from "storybook/internal/csf"; import type { TestCase, TestModule, TestSuite, BrowserCommand, BrowserCommandContext, -} from 'vitest/node'; -import { type PlaywrightProviderOptions } from '@vitest/browser-playwright'; -import { type serializedNodeWithId } from '@rrweb/types'; -import { ResourceArchiver, writeTestResult, type DOMSnapshots } from '@chromatic-com/shared-e2e'; +} from "vitest/node"; + import { type StoryParameters, type ChromaticNamespace, type ConfigureOptions, type ResolvedOptions, -} from '../types'; -import { trackEvent } from './telemetry'; -import { NetworkIdleTracker } from './NetworkIdleTracker'; -import { ChromaticReporter } from './reporter'; -import { TelemetryReporter } from './telemetry'; -import { WebpackStatsReporter } from './webpack-stats-reporter'; - -type TestID = TestCase['id']; -type SessionId = BrowserCommandContext['sessionId']; +} from "../types"; +import { NetworkIdleTracker } from "./NetworkIdleTracker"; +import { ChromaticReporter } from "./reporter"; +import { trackEvent } from "./telemetry"; +import { TelemetryReporter } from "./telemetry"; +import { WebpackStatsReporter } from "./webpack-stats-reporter"; + +type TestID = TestCase["id"]; +type SessionId = BrowserCommandContext["sessionId"]; type SnapshotName = keyof DOMSnapshots; export function createCommands(options: ResolvedOptions) { @@ -38,9 +40,9 @@ export function createCommands(options: ResolvedOptions) { SnapshotName, { snapshot: serializedNodeWithId; - viewport: DOMSnapshots[string]['viewport']; - colorScheme: DOMSnapshots[string]['colorScheme']; - pseudoClassIds: DOMSnapshots[string]['pseudoClassIds']; + viewport: DOMSnapshots[string]["viewport"]; + colorScheme: DOMSnapshots[string]["colorScheme"]; + pseudoClassIds: DOMSnapshots[string]["pseudoClassIds"]; } > >(); @@ -54,14 +56,14 @@ export function createCommands(options: ResolvedOptions) { context, id: TestID, snapshot: serializedNodeWithId, - pseudoClassIds: DOMSnapshots[string]['pseudoClassIds'], + pseudoClassIds: DOMSnapshots[string]["pseudoClassIds"], name: string | undefined, - snapshotOptions?: { isAutomaticSnapshot: boolean } + snapshotOptions?: { isAutomaticSnapshot: boolean }, ) { const entity = context.project.vitest.state.getReportedEntityById(id); assert( - entity?.type === 'test', - `Expected entity with id ${id} to be a test, found ${entity?.type}` + entity?.type === "test", + `Expected entity with id ${id} to be a test, found ${entity?.type}`, ); let sessionSnapshots = snapshots.get(id); @@ -79,10 +81,10 @@ export function createCommands(options: ResolvedOptions) { () => ({ viewport: { width: window.innerWidth, height: window.innerHeight }, - colorScheme: window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light', - }) as const + colorScheme: window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light", + }) as const, ); sessionSnapshots.set(name, { snapshot, viewport, colorScheme, pseudoClassIds }); @@ -129,7 +131,7 @@ export function createCommands(options: ResolvedOptions) { cdp, options.assetDomains, contextOptions?.httpCredentials, - new URL(context.page.url()) + new URL(context.page.url()), ); resourceArchivers.set(context.sessionId, resourceArchiver); @@ -151,8 +153,8 @@ export function createCommands(options: ResolvedOptions) { async __chromatic_writeTestResult(context, id: TestID, testOptions: ConfigureOptions = {}) { const entity = context.project.vitest.state.getReportedEntityById(id); assert( - entity?.type === 'test', - `Expected entity with id ${id} to be a test, found ${entity?.type}` + entity?.type === "test", + `Expected entity with id ${id} to be a test, found ${entity?.type}`, ); const { archive, sessionSnapshots } = await onTestCleanup(context, id); @@ -203,7 +205,7 @@ export function createCommands(options: ResolvedOptions) { cropToViewport: testOptions.cropToViewport ?? options.cropToViewport, ignoreSelectors: testOptions.ignoreSelectors ?? options.ignoreSelectors, }, - 'vitest' + "vitest", ); if (options.turboSnap) { @@ -212,12 +214,12 @@ export function createCommands(options: ResolvedOptions) { trackEvent( { - eventType: 'archives_created', - level: 'info', + eventType: "archives_created", + level: "info", payload: { archiveCount: Object.keys(snapshotBuffers).length }, }, context.project.vitest, - options + options, ); }, @@ -232,7 +234,7 @@ export function createCommands(options: ResolvedOptions) { * Forward a browser-side telemetry event to the Node side. */ async __chromatic_telemetry(context, event: Parameters[0]) { - void trackEvent(event, context.project.vitest, options); + trackEvent(event, context.project.vitest, options); }, /** @@ -274,18 +276,18 @@ export function createCommands(options: ResolvedOptions) { throw error; } }, - ] - ) + ], + ), ) as typeof commands; } function trackCommandFailure( command: keyof ChromaticCommands, error: unknown, - context: BrowserCommandContext + context: BrowserCommandContext, ) { // Don't tracked failed telemetry events or test-only getSnapshots command - if (command === '__chromatic_telemetry' || command === '__chromatic_getSnapshots') { + if (command === "__chromatic_telemetry" || command === "__chromatic_getSnapshots") { return; } @@ -299,9 +301,9 @@ export function createCommands(options: ResolvedOptions) { try { trackEvent( - { eventType: 'command_failed', level: 'error', payload: { command, error } }, + { eventType: "command_failed", level: "error", payload: { command, error } }, context.project.vitest, - options + options, ); } catch { // Make sure failing trackEvent doesn't hide the original error @@ -328,8 +330,8 @@ export function createCommands(options: ResolvedOptions) { } function generateUniqueSnapshotName(options: { snapshotName: string[]; titlePath: string[] }) { - const names = options.snapshotName.join(' / '); - const base = toId(options.titlePath.join(' / '), storyNameFromExport(names)); + const names = options.snapshotName.join(" / "); + const base = toId(options.titlePath.join(" / "), storyNameFromExport(names)); let key = base; let count = 1; @@ -352,7 +354,7 @@ function getTitle(test: TestCase): string[] { // If Vitest was configured with multiple projects, namespace the results with project name const hasManyProjects = - test.project.vitest.projects.filter((project) => project.config.browser.name === 'chromium') + test.project.vitest.projects.filter((project) => project.config.browser.name === "chromium") .length > 1; if (hasManyProjects && test.project.name) { @@ -366,10 +368,10 @@ function getSnapshotPrefix(test: TestCase): string[] { const names = [test.name]; let current: TestCase | TestSuite | TestModule = test; - while ('parent' in current && current.parent) { + while ("parent" in current && current.parent) { current = current.parent; - if ('name' in current && current.name) { + if ("name" in current && current.name) { names.unshift(current.name); } } @@ -378,17 +380,17 @@ function getSnapshotPrefix(test: TestCase): string[] { } function getSuiteNames(test: TestCase): string[] { - if (test.parent.type !== 'suite') { + if (test.parent.type !== "suite") { return []; } const names = []; let current: TestCase | TestSuite | TestModule = test; - while ('parent' in current && current.parent) { + while ("parent" in current && current.parent) { current = current.parent; - if (current.type === 'suite') { + if (current.type === "suite") { names.unshift(current.name); } } @@ -397,19 +399,19 @@ function getSuiteNames(test: TestCase): string[] { } function removeFileExtensions(filepath: string) { - const parts = filepath.split('/'); - const filename = parts.pop() || ''; + const parts = filepath.split("/"); + const filename = parts.pop() || ""; return parts .concat( // Trim filenames like "src/components/accordion/Accordion.browser.spec.tsx" -> "src/components/accordion/Accordion" - filename.split('.')[0] + filename.split(".")[0], ) - .join('/'); + .join("/"); } /** @internal */ -declare module 'vitest/browser' { +declare module "vitest/browser" { // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface BrowserCommands extends ChromaticCommands {} } diff --git a/packages/vitest/src/node/plugin.browser.test.ts b/packages/vitest/src/node/plugin.browser.test.ts index f232920c..e587a8a9 100644 --- a/packages/vitest/src/node/plugin.browser.test.ts +++ b/packages/vitest/src/node/plugin.browser.test.ts @@ -1,7 +1,7 @@ -import { test, expect, inject } from 'vitest'; +import { test, expect, inject } from "vitest"; -test('provided __chromatic_options', async () => { - expect(inject('__chromatic_options')).toMatchInlineSnapshot(` +test("provided __chromatic_options", async () => { + expect(inject("__chromatic_options")).toMatchInlineSnapshot(` { "assetDomains": [], "disableAutoSnapshot": false, diff --git a/packages/vitest/src/node/plugin.test.ts b/packages/vitest/src/node/plugin.test.ts index ddb58111..8d4d1b2f 100644 --- a/packages/vitest/src/node/plugin.test.ts +++ b/packages/vitest/src/node/plugin.test.ts @@ -1,35 +1,37 @@ -import { resolve } from 'node:path'; -import { mkdir, readdir, rm } from 'node:fs/promises'; -import { beforeEach, expect, onTestFinished, test } from 'vitest'; -import { createVitest, type TestModule } from 'vitest/node'; -import { uniqueId } from '@chromatic-com/shared-e2e/write-archive/stories-files'; -import { chromaticPlugin } from './plugin'; +import { mkdir, readdir, rm } from "node:fs/promises"; +import { resolve } from "node:path"; + +import { uniqueId } from "@chromatic-com/shared-e2e/write-archive/stories-files"; +import { beforeEach, expect, onTestFinished, test } from "vitest"; +import { createVitest, type TestModule } from "vitest/node"; + import { createOutputStreams, getBrowserConfig, getResolvedConfig, runFixture, setupTelemetryServer, -} from '../../test/utils/node'; +} from "../../test/utils/node"; +import { chromaticPlugin } from "./plugin"; beforeEach(() => { uniqueId.value = 1; }); test.each([ - { name: 'string', setupFiles: 'some-user-defined-setup.ts' }, - { name: 'array', setupFiles: ['some-user-defined-setup.ts'] }, + { name: "string", setupFiles: "some-user-defined-setup.ts" }, + { name: "array", setupFiles: ["some-user-defined-setup.ts"] }, ])("preserves user's $name setupFiles", async ({ setupFiles }) => { const config = await getResolvedConfig({ setupFiles }); - const result = config.setupFiles.map((s) => s.replace(process.cwd(), '')); + const result = config.setupFiles.map((s) => s.replace(process.cwd(), "")); expect.soft(result).toHaveLength(2); - expect.soft(result[0]).toBe('/some-user-defined-setup.ts'); - expect.soft(result[1]).toBe('/packages/vitest/src/browser/setupFile.ts'); + expect.soft(result[0]).toBe("/some-user-defined-setup.ts"); + expect.soft(result[1]).toBe("/packages/vitest/src/browser/setupFile.ts"); }); -test('adds browser commands', async () => { +test("adds browser commands", async () => { const config = await getResolvedConfig(); expect(config.browser.commands).toMatchInlineSnapshot(` @@ -46,8 +48,8 @@ test('adds browser commands', async () => { `); }); -test('adds tags', async () => { - const config = await getResolvedConfig(undefined, { tags: ['my-tag-for-vrt'] }); +test("adds tags", async () => { + const config = await getResolvedConfig(undefined, { tags: ["my-tag-for-vrt"] }); expect(config.tags).toMatchInlineSnapshot(` [ @@ -59,12 +61,12 @@ test('adds tags', async () => { `); }); -test('does not override user-defined tags', async () => { +test("does not override user-defined tags", async () => { const config = await getResolvedConfig( { - tags: [{ name: 'my-tag-for-vrt', description: 'Custom description' }], + tags: [{ name: "my-tag-for-vrt", description: "Custom description" }], }, - { tags: ['my-tag-for-vrt'] } + { tags: ["my-tag-for-vrt"] }, ); expect(config.tags).toMatchInlineSnapshot(` @@ -77,40 +79,40 @@ test('does not override user-defined tags', async () => { `); }); -test('warns if tags are used with Vitest 4.0', async () => { +test("warns if tags are used with Vitest 4.0", async () => { const { cleanup, onRequest } = setupTelemetryServer(); onTestFinished(cleanup); const { streams, getOutput } = createOutputStreams(); - const plugin = chromaticPlugin({ tags: ['my-tag-for-vrt'] }); + const plugin = chromaticPlugin({ tags: ["my-tag-for-vrt"] }); const vitest = await createVitest( - 'test', + "test", { config: false, standalone: true, watch: true }, {}, - streams + streams, ); onTestFinished(() => vitest.close()); const project = vitest.projects[0]; project.config.browser.enabled = true; - project.config.browser.name = 'chromium'; + project.config.browser.name = "chromium"; // @ts-expect-error -- intentional - vitest.version = '4.0.1'; + vitest.version = "4.0.1"; // @ts-expect-error -- intentional plugin.configureVitest?.({ vitest, project }); expect(getOutput().stderr).toContain( - 'chromatic Tags cannot be used with Vitest 4.0.1. Please upgrade to Vitest 4.1 or later to use this feature.' + "chromatic Tags cannot be used with Vitest 4.0.1. Please upgrade to Vitest 4.1 or later to use this feature.", ); await vitest.close(); const tagsLowVersionEvents = onRequest.mock.calls.flatMap(([event]) => - event.eventType === 'vitest_tags_low_version' + event.eventType === "vitest_tags_low_version" ? [{ eventType: event.eventType, payload: event.payload, level: event.level }] - : [] + : [], ); expect(tagsLowVersionEvents).toMatchInlineSnapshot(` @@ -124,7 +126,7 @@ test('warns if tags are used with Vitest 4.0', async () => { `); }); -test('can be scoped to a Vitest project', async () => { +test("can be scoped to a Vitest project", async () => { const tests: TestModule[] = []; await runFixture( @@ -134,34 +136,34 @@ test('can be scoped to a Vitest project', async () => { projects: [ { test: { - name: 'unit', + name: "unit", /** See {@link file://./../../test/fixtures/node-environment.test.ts} */ - include: ['**/node-environment.test.ts'], - root: resolve(import.meta.dirname, '../../test/fixtures'), + include: ["**/node-environment.test.ts"], + root: resolve(import.meta.dirname, "../../test/fixtures"), }, }, { plugins: [chromaticPlugin()], test: { - name: 'vrt', + name: "vrt", /** See {@link file://./../../test/fixtures/dom.test.ts} */ - include: ['**/dom.test.ts'], - root: resolve(import.meta.dirname, '../../test/fixtures'), + include: ["**/dom.test.ts"], + root: resolve(import.meta.dirname, "../../test/fixtures"), browser: getBrowserConfig(), }, }, ], }, - { disabled: true } + { disabled: true }, ); expect(tests).toHaveLength(2); - expect(tests[0].state()).toBe('passed'); - expect(tests[1].state()).toBe('passed'); + expect(tests[0].state()).toBe("passed"); + expect(tests[1].state()).toBe("passed"); }); -test('writes results to root when in Vitest projects setup', async () => { - const root = resolve(import.meta.dirname, '../../test/custom-root'); +test("writes results to root when in Vitest projects setup", async () => { + const root = resolve(import.meta.dirname, "../../test/custom-root"); await mkdir(root, { recursive: true }); onTestFinished(() => rm(root, { recursive: true, force: true })); @@ -173,29 +175,29 @@ test('writes results to root when in Vitest projects setup', async () => { { plugins: [chromaticPlugin()], test: { - name: 'first-project', - browser: getBrowserConfig('first-browser'), - include: ['**/dom.test.ts'], - root: resolve(import.meta.dirname, '../../test/fixtures'), + name: "first-project", + browser: getBrowserConfig("first-browser"), + include: ["**/dom.test.ts"], + root: resolve(import.meta.dirname, "../../test/fixtures"), sequence: { groupOrder: 1 }, }, }, { plugins: [chromaticPlugin()], test: { - name: 'second-project', - browser: getBrowserConfig('second-browser'), - include: ['**/dom.test.ts'], - root: resolve(import.meta.dirname, '../../test/fixtures'), + name: "second-project", + browser: getBrowserConfig("second-browser"), + include: ["**/dom.test.ts"], + root: resolve(import.meta.dirname, "../../test/fixtures"), sequence: { groupOrder: 2 }, }, }, ], }, - { disabled: true } + { disabled: true }, ); - const results = await readdir(resolve(root, '.vitest/chromatic/chromatic-archives')); + const results = await readdir(resolve(root, ".vitest/chromatic/chromatic-archives")); // 1 for "archive" directory and 2 for each project's "*.stories.json" files expect.soft(results).toHaveLength(3); @@ -209,7 +211,7 @@ test('writes results to root when in Vitest projects setup', async () => { `); }); -test('skips configuration when used on non-browser context', async () => { +test("skips configuration when used on non-browser context", async () => { const config = await getResolvedConfig({ browser: { enabled: false }, }); @@ -220,27 +222,27 @@ test('skips configuration when used on non-browser context', async () => { test('does not clean existing output directory when "vitest --merge-reports" is run', async () => { const options = { /** See {@link file://./../../test/fixtures/dom.test.ts} */ - include: ['**/dom.test.ts'], - root: resolve(import.meta.dirname, '../../test/fixtures'), + include: ["**/dom.test.ts"], + root: resolve(import.meta.dirname, "../../test/fixtures"), }; - const outputDir = resolve(options.root, '.vitest-reports'); - const outputFile = resolve(outputDir, 'blob.json'); + const outputDir = resolve(options.root, ".vitest-reports"); + const outputFile = resolve(outputDir, "blob.json"); await rm(outputDir, { recursive: true, force: true }); // First run to generate blob - await runFixture({ reporters: [['blob', { outputFile }]], ...options }); + await runFixture({ reporters: [["blob", { outputFile }]], ...options }); // Second with --merge-reports to see if .vitest/chromatic is accidentally removed const { stdout, stderr } = await runFixture({ mergeReports: outputDir, ...options }); - expect(stdout).toContain('Test Files 1 passed (1)\n'); - expect(stdout).toContain('Tests 1 passed (1)\n'); - expect(stderr).toBe(''); + expect(stdout).toContain("Test Files 1 passed (1)\n"); + expect(stdout).toContain("Tests 1 passed (1)\n"); + expect(stderr).toBe(""); // Chromatic results should preserve on file system - const archives = resolve(options.root, '.vitest/chromatic/chromatic-archives'); + const archives = resolve(options.root, ".vitest/chromatic/chromatic-archives"); await expect(readdir(archives)).resolves.toMatchInlineSnapshot(` [ "archive", @@ -249,36 +251,36 @@ test('does not clean existing output directory when "vitest --merge-reports" is `); }); -test('works in multi project instance setup', { timeout: 30_000 }, async () => { - const root = resolve(import.meta.dirname, '../../test/fixtures'); +test("works in multi project instance setup", { timeout: 30_000 }, async () => { + const root = resolve(import.meta.dirname, "../../test/fixtures"); const tests: TestModule[] = []; const { stderr } = await runFixture({ - name: 'custom-project-name', + name: "custom-project-name", browser: { ...getBrowserConfig(), instances: [ - { browser: 'chromium', name: 'custom-name-for-chromium-browser' }, - { browser: 'webkit', name: 'custom-name-for-webkit-browser' }, - { browser: 'firefox', name: 'custom-name-for-firefox-browser' }, + { browser: "chromium", name: "custom-name-for-chromium-browser" }, + { browser: "webkit", name: "custom-name-for-webkit-browser" }, + { browser: "firefox", name: "custom-name-for-firefox-browser" }, ], }, - reporters: ['default', { onTestRunEnd: (testModules) => void tests.push(...testModules) }], + reporters: ["default", { onTestRunEnd: (testModules) => void tests.push(...testModules) }], /** See {@link file://./../../test/fixtures/public-apis.test.ts} */ - include: ['**/public-apis.test.ts'], + include: ["**/public-apis.test.ts"], root, }); - expect(stderr).toBe(''); + expect(stderr).toBe(""); // Non-Chromium browsers should not crash expect.soft(tests).toHaveLength(3); - expect.soft(tests[0].state()).toBe('passed'); - expect.soft(tests[1].state()).toBe('passed'); - expect.soft(tests[2].state()).toBe('passed'); + expect.soft(tests[0].state()).toBe("passed"); + expect.soft(tests[1].state()).toBe("passed"); + expect.soft(tests[2].state()).toBe("passed"); // Results for Chromium should still be written to disk - const results = await readdir(resolve(root, '.vitest/chromatic/chromatic-archives')); + const results = await readdir(resolve(root, ".vitest/chromatic/chromatic-archives")); expect(results).toMatchInlineSnapshot(` [ diff --git a/packages/vitest/src/node/plugin.ts b/packages/vitest/src/node/plugin.ts index 7921f271..d0053c49 100644 --- a/packages/vitest/src/node/plugin.ts +++ b/packages/vitest/src/node/plugin.ts @@ -1,11 +1,16 @@ -import { existsSync, readdirSync, rmSync } from 'node:fs'; -import { resolve } from 'node:path'; -import type {} from 'vitest/config'; -import type { Vite } from 'vitest/node'; -import { version as vitestVersion } from 'vitest/node'; -import colors from 'tinyrainbow'; -import { DEFAULT_GLOBAL_RESOURCE_ARCHIVE_TIMEOUT_MS } from '@chromatic-com/shared-e2e'; -import { createCommands } from './commands'; +import { existsSync, readdirSync, rmSync } from "node:fs"; +import { resolve } from "node:path"; + +import { DEFAULT_GLOBAL_RESOURCE_ARCHIVE_TIMEOUT_MS } from "@chromatic-com/shared-e2e"; +import colors from "tinyrainbow"; +import type {} from "vitest/config"; +import type { Vite } from "vitest/node"; +import { version as vitestVersion } from "vitest/node"; + +import { DEFAULT_OUTPUT_DIR } from "../constants"; +import { type ResolvedOptions, type Options } from "../types"; +import { createCommands } from "./commands"; +import { ChromaticReporter } from "./reporter"; import { cleanTelemetryLogFiles, resolveTelemetryOptions, @@ -13,14 +18,11 @@ import { trackEvent as _trackEvent, type EventType, type TelemetryEvent, -} from './telemetry'; -import { ChromaticReporter } from './reporter'; -import { TelemetryReporter } from './telemetry'; -import { mergePreviewStats, WebpackStatsReporter } from './webpack-stats-reporter'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; -import { type ResolvedOptions, type Options } from '../types'; +} from "./telemetry"; +import { TelemetryReporter } from "./telemetry"; +import { mergePreviewStats, WebpackStatsReporter } from "./webpack-stats-reporter"; -const DEFAULT_TAG_DESCRIPTION = 'Visual Regression Tests for `@chromatic-com/vitest`'; +const DEFAULT_TAG_DESCRIPTION = "Visual Regression Tests for `@chromatic-com/vitest`"; /** * Vitest plugin for integrating with Chromatic's visual regression testing. @@ -38,15 +40,15 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { telemetry: resolveTelemetryOptions(), }; - const isDist = import.meta.url.includes('dist/plugin.mjs'); + const isDist = import.meta.url.includes("dist/plugin.mjs"); const setupFile = resolve( import.meta.dirname, - isDist ? './setupFile.mjs' : '../browser/setupFile.ts' + isDist ? "./setupFile.mjs" : "../browser/setupFile.ts", ); return { - name: 'vitest:chromatic', + name: "vitest:chromatic", config() { return { @@ -55,9 +57,9 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { }, // vitest/suite is available in 4.0 only. Importing it in 4.1 logs error, in 5.0 it's removed. - resolve: vitestVersion.startsWith('4.0') + resolve: vitestVersion.startsWith("4.0") ? undefined - : { alias: { 'vitest/suite': 'vitest' } }, + : { alias: { "vitest/suite": "vitest" } }, test: { provide: { @@ -82,8 +84,8 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { setupTelemetryCleanup(context.vitest); trackEvent({ - eventType: 'plugin_configured', - level: 'info', + eventType: "plugin_configured", + level: "info", payload: { isShardedRun: project.config.shard != null, cropToViewport: options.cropToViewport, @@ -98,10 +100,10 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { resourceArchiveTimeout: options.resourceArchiveTimeout, turboSnap: options.turboSnap, reporter: !options.reporter.enabled - ? 'off' + ? "off" : options.reporter.verbose - ? 'verbose' - : 'non-verbose', + ? "verbose" + : "non-verbose", // Don't attach any user-defined strings values: isCustomOutputDirectory: options.outputDirectory !== DEFAULT_OUTPUT_DIR, @@ -117,11 +119,11 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { } // browser.name is instances[].browser, not instances[].name: https://github.com/vitest-dev/vitest/blob/d22b029ae056b9515033d75c1249e9db26612770/packages/vitest/src/node/projects/resolveProjects.ts#L307 - if (!browser.enabled || browser.name !== 'chromium') { + if (!browser.enabled || browser.name !== "chromium") { trackEvent({ - eventType: 'project_ineligible', - level: 'warn', - payload: { isBrowser: browser.enabled, isChromium: browser.name === 'chromium' }, + eventType: "project_ineligible", + level: "warn", + payload: { isBrowser: browser.enabled, isChromium: browser.name === "chromium" }, }); return clean(); @@ -138,37 +140,37 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { } // Ensure our setup file is registered first so that afterEach runs before any user-defined hooks. - if (sequence.hooks === 'stack') { + if (sequence.hooks === "stack") { project.config.setupFiles.push(setupFile); - } else if (sequence.hooks === 'list') { + } else if (sequence.hooks === "list") { project.config.setupFiles.unshift(setupFile); } else { trackEvent({ - eventType: 'setup_files_parallel', - level: 'warn', + eventType: "setup_files_parallel", + level: "warn", payload: { setupFileCount: project.config.setupFiles.length }, }); project.config.setupFiles.push(setupFile); context.vitest.logger.warn( - colors.bgYellow(colors.black(' chromatic ')), + colors.bgYellow(colors.black(" chromatic ")), colors.yellow( - `Using { sequence.hooks: 'parallel' } may cause unstable snapshots. Please set 'sequence.hooks' to 'list' or 'stack' to ensure reliable snapshot ordering.` - ) + `Using { sequence.hooks: 'parallel' } may cause unstable snapshots. Please set 'sequence.hooks' to 'list' or 'stack' to ensure reliable snapshot ordering.`, + ), ); } // We support Vitest 4.0.0, but tags were introduced in 4.1.0 - if (options.tags && context.vitest.version.startsWith('4.0')) { + if (options.tags && context.vitest.version.startsWith("4.0")) { context.vitest.logger.warn( - colors.bgYellow(colors.black(' chromatic ')), + colors.bgYellow(colors.black(" chromatic ")), colors.yellow( - `Tags cannot be used with Vitest ${context.vitest.version}. Please upgrade to Vitest 4.1 or later to use this feature.` - ) + `Tags cannot be used with Vitest ${context.vitest.version}. Please upgrade to Vitest 4.1 or later to use this feature.`, + ), ); - trackEvent({ eventType: 'tags_low_version', level: 'warn', payload: {} }); + trackEvent({ eventType: "tags_low_version", level: "warn", payload: {} }); } if (options.tags) { @@ -192,9 +194,9 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { }); } catch (error) { trackEvent({ - eventType: 'turbosnap_error', - level: 'error', - payload: { operation: 'merge-stats', error }, + eventType: "turbosnap_error", + level: "error", + payload: { operation: "merge-stats", error }, }); throw error; @@ -206,18 +208,18 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { project.onTestsRerun(async () => { clean(); - await project.browser?.triggerCommand('__chromatic_reset', {} as any); + await project.browser?.triggerCommand("__chromatic_reset", {} as any); }); function clean() { const outputDirectory = resolve(project.vitest.config.root, options.outputDirectory); - rmSync(resolve(outputDirectory, 'chromatic-archives'), { recursive: true, force: true }); + rmSync(resolve(outputDirectory, "chromatic-archives"), { recursive: true, force: true }); cleanTelemetryLogFiles(outputDirectory); if (existsSync(outputDirectory)) { for (const file of readdirSync(outputDirectory)) { - if (file.startsWith('preview-stats') && file.endsWith('.json')) { + if (file.startsWith("preview-stats") && file.endsWith(".json")) { rmSync(resolve(outputDirectory, file), { force: true }); } } @@ -233,16 +235,17 @@ export function chromaticPlugin(userOptions: Options = {}): Vite.Plugin { function withErrorTracking( options: ResolvedOptions, - configureVitest: Vite.Plugin['configureVitest'] -): Vite.Plugin['configureVitest'] { + configureVitest: Vite.Plugin["configureVitest"], +): Vite.Plugin["configureVitest"] { return async (context) => { try { + // oxlint-disable-next-line typescript/await-thenable -- this is really promise return await configureVitest(context); } catch (error) { _trackEvent( - { eventType: 'plugin_error', level: 'error', payload: { operation: 'configure', error } }, + { eventType: "plugin_error", level: "error", payload: { operation: "configure", error } }, context.vitest, - options + options, ); throw error; @@ -250,7 +253,7 @@ function withErrorTracking( }; } -function resolveReporterOptions(reporter: Options['reporter']): ResolvedOptions['reporter'] { +function resolveReporterOptions(reporter: Options["reporter"]): ResolvedOptions["reporter"] { if (reporter == undefined || reporter === true) { return { enabled: true, verbose: true }; } @@ -266,7 +269,7 @@ function resolveReporterOptions(reporter: Options['reporter']): ResolvedOptions[ } /** @internal */ -declare module 'vitest' { +declare module "vitest" { export interface ProvidedContext { __chromatic_options: ResolvedOptions; } diff --git a/packages/vitest/src/node/reporter.test.ts b/packages/vitest/src/node/reporter.test.ts index b1a72478..fae530a2 100644 --- a/packages/vitest/src/node/reporter.test.ts +++ b/packages/vitest/src/node/reporter.test.ts @@ -1,23 +1,25 @@ -import { existsSync, rmSync } from 'node:fs'; -import { normalize, resolve } from 'node:path'; -import { expect, onTestFinished, test, vi } from 'vitest'; -import * as shared from '@chromatic-com/shared-e2e'; +import { existsSync, rmSync } from "node:fs"; +import { normalize, resolve } from "node:path"; + +import * as shared from "@chromatic-com/shared-e2e"; +import { expect, onTestFinished, test, vi } from "vitest"; + import { runFixture as baseRunFixture, getBrowserConfig, isVitest5, setupTelemetryServer, StableTestFileOrderSorter, -} from '../../test/utils/node'; -import { DEFAULT_OUTPUT_DIR } from '../constants'; +} from "../../test/utils/node"; +import { DEFAULT_OUTPUT_DIR } from "../constants"; -vi.mock('@chromatic-com/shared-e2e'); +vi.mock("@chromatic-com/shared-e2e"); vi.mocked(shared.writeTestResult).mockImplementation(() => - Promise.resolve({ storiesFile: 'test.stories.json' }) + Promise.resolve({ storiesFile: "test.stories.json" }), ); -test('default reporter', async () => { - const { stdout } = await runFixture({ reporters: 'default' }); +test("default reporter", async () => { + const { stdout } = await runFixture({ reporters: "default" }); expect(trimReporterOutput(stdout)).toMatchInlineSnapshot(` " RUN v[...] /packages/vitest/test/fixtures @@ -32,9 +34,9 @@ test('default reporter', async () => { `); }); -test('default reporter with slow tests', { timeout: 10_000 }, async () => { +test("default reporter with slow tests", { timeout: 10_000 }, async () => { const { stdout } = await runFixture({ - reporters: 'default', + reporters: "default", provide: { delay: 500 }, slowTestThreshold: 499, }); @@ -78,8 +80,8 @@ test('default reporter with slow tests', { timeout: 10_000 }, async () => { } }); -test('tree reporter', async () => { - const { stdout } = await runFixture({ reporters: 'tree' }); +test("tree reporter", async () => { + const { stdout } = await runFixture({ reporters: "tree" }); expect(trimReporterOutput(stdout)).toMatchInlineSnapshot(` " RUN v[...] /packages/vitest/test/fixtures @@ -106,8 +108,8 @@ test('tree reporter', async () => { `); }); -test('verbose reporter', async () => { - const { stdout } = await runFixture({ reporters: 'verbose' }); +test("verbose reporter", async () => { + const { stdout } = await runFixture({ reporters: "verbose" }); expect(trimReporterOutput(stdout)).toMatchInlineSnapshot(` " RUN v[...] /packages/vitest/test/fixtures @@ -140,22 +142,22 @@ test('verbose reporter', async () => { `); }); -test.each(['default', 'tree', 'verbose'] as const)( - '%s reporter with { verbose: false }', +test.each(["default", "tree", "verbose"] as const)( + "%s reporter with { verbose: false }", async (builtInReporter) => { const { stdout } = await runFixture( { reporters: builtInReporter }, - { reporter: { verbose: false } } + { reporter: { verbose: false } }, ); const output = trimReporterOutput(stdout); - expect(output).not.toContain('archive'); - expect(output).not.toContain('captured'); - } + expect(output).not.toContain("archive"); + expect(output).not.toContain("captured"); + }, ); -test('summary when default root', async () => { +test("summary when default root", async () => { onTestFinished(() => { const reports = resolve(process.cwd(), DEFAULT_OUTPUT_DIR); @@ -164,7 +166,7 @@ test('summary when default root', async () => { } }); - const { stdout } = await runFixture({ reporters: 'default', root: process.cwd() }); + const { stdout } = await runFixture({ reporters: "default", root: process.cwd() }); expect(trimSummary(stdout)).toMatchInlineSnapshot(` "Chromatic Visual Regression @@ -176,8 +178,8 @@ test('summary when default root', async () => { `); }); -test('summary when custom root', async () => { - const { stdout } = await runFixture({ reporters: 'default' }); +test("summary when custom root", async () => { + const { stdout } = await runFixture({ reporters: "default" }); expect(trimSummary(stdout)).toMatchInlineSnapshot(` "Chromatic Visual Regression @@ -189,7 +191,7 @@ test('summary when custom root', async () => { `); }); -test('summary with TurboSnap enabled', async () => { +test("summary with TurboSnap enabled", async () => { onTestFinished(() => { const reports = resolve(process.cwd(), DEFAULT_OUTPUT_DIR); @@ -199,8 +201,8 @@ test('summary with TurboSnap enabled', async () => { }); const { stdout } = await runFixture( - { reporters: 'default', root: process.cwd() }, - { turboSnap: true } + { reporters: "default", root: process.cwd() }, + { turboSnap: true }, ); expect(trimSummary(stdout)).toMatchInlineSnapshot(` @@ -213,9 +215,9 @@ test('summary with TurboSnap enabled', async () => { `); }); -test('summary with `CHROMATIC_TELEMETRY_LOG_TO_FILE` enabled', async () => { +test("summary with `CHROMATIC_TELEMETRY_LOG_TO_FILE` enabled", async () => { const { cleanup } = setupTelemetryServer(); - vi.stubEnv('CHROMATIC_TELEMETRY_LOG_TO_FILE', '1'); + vi.stubEnv("CHROMATIC_TELEMETRY_LOG_TO_FILE", "1"); onTestFinished(() => { vi.unstubAllEnvs(); @@ -228,7 +230,7 @@ test('summary with `CHROMATIC_TELEMETRY_LOG_TO_FILE` enabled', async () => { } }); - const { stdout } = await runFixture({ reporters: 'default', root: process.cwd() }); + const { stdout } = await runFixture({ reporters: "default", root: process.cwd() }); expect(trimSummary(stdout)).toMatchInlineSnapshot(` "Chromatic Visual Regression @@ -241,8 +243,8 @@ test('summary with `CHROMATIC_TELEMETRY_LOG_TO_FILE` enabled', async () => { `); }); -test('summary when custom output directory', async () => { - const outputDirectory = '.vitest/custom-output-directory'; +test("summary when custom output directory", async () => { + const outputDirectory = ".vitest/custom-output-directory"; onTestFinished(() => { const reports = resolve(process.cwd(), outputDirectory); @@ -253,8 +255,8 @@ test('summary when custom output directory', async () => { }); const { stdout } = await runFixture( - { reporters: 'default', root: process.cwd() }, - { outputDirectory } + { reporters: "default", root: process.cwd() }, + { outputDirectory }, ); expect(trimSummary(stdout)).toMatchInlineSnapshot(` @@ -267,10 +269,10 @@ test('summary when custom output directory', async () => { `); }); -test('summary when no eligible projects', { timeout: 15_000 }, async () => { +test("summary when no eligible projects", { timeout: 15_000 }, async () => { const { stdout } = await runFixture({ - reporters: 'default', - browser: { ...getBrowserConfig(), instances: [{ browser: 'webkit' }] }, + reporters: "default", + browser: { ...getBrowserConfig(), instances: [{ browser: "webkit" }] }, }); expect(trimSummary(stdout)).toMatchInlineSnapshot(` @@ -280,8 +282,8 @@ test('summary when no eligible projects', { timeout: 15_000 }, async () => { `); }); -test('reporter can be disabled', async () => { - const { stdout } = await runFixture({ reporters: 'default' }, { reporter: false }); +test("reporter can be disabled", async () => { + const { stdout } = await runFixture({ reporters: "default" }, { reporter: false }); if (isVitest5()) { expect(trimReporterOutput(stdout, 0, Infinity)).toMatchInlineSnapshot(` @@ -316,7 +318,7 @@ test('reporter can be disabled', async () => { function runFixture( options: Parameters[0], - pluginOptions?: Parameters[1] + pluginOptions?: Parameters[1], ) { return baseRunFixture( { @@ -324,43 +326,43 @@ function runFixture( sequence: { sequencer: StableTestFileOrderSorter }, include: [ /** {@link file://./../../test/fixtures/reporter-1.test.ts} */ - resolve(import.meta.dirname, '../../test/fixtures/reporter-1.test.ts'), + resolve(import.meta.dirname, "../../test/fixtures/reporter-1.test.ts"), /** {@link file://./../../test/fixtures/reporter-2.test.ts} */ - resolve(import.meta.dirname, '../../test/fixtures/reporter-2.test.ts'), + resolve(import.meta.dirname, "../../test/fixtures/reporter-2.test.ts"), /** {@link file://./../../test/fixtures/reporter-3.test.ts} */ - resolve(import.meta.dirname, '../../test/fixtures/reporter-3.test.ts'), + resolve(import.meta.dirname, "../../test/fixtures/reporter-3.test.ts"), ], ...options, }, - pluginOptions + pluginOptions, ); } function trimReporterOutput(report: string, start?: number, end?: number) { - const lines = report.split('\n'); - const startIndex = start ?? lines.findIndex((line) => line.includes('RUN v')); - const endIndex = end ?? lines.findIndex((line) => line.includes('Test Files')); + const lines = report.split("\n"); + const startIndex = start ?? lines.findIndex((line) => line.includes("RUN v")); + const endIndex = end ?? lines.findIndex((line) => line.includes("Test Files")); return lines .slice(startIndex, endIndex) - .join('\n') - .replaceAll(/\d+ms/g, '