diff --git a/.changeset/drop-deno-docs.md b/.changeset/drop-deno-docs.md new file mode 100644 index 0000000..100465a --- /dev/null +++ b/.changeset/drop-deno-docs.md @@ -0,0 +1,5 @@ +--- +"@hebilicious/cssforge": patch +--- + +Document the JSR channel for Node package managers only. The README no longer describes Deno usage; the package is still published to JSR at the same version as npm. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e6e3a1f..e825abf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -108,26 +108,3 @@ jobs: - name: Smoke test the packed package on the supported engine floor run: moon run cssforge:smoke-test -- --node "$(command -v node)" - - jsr-entries: - name: JSR entry points - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup toolchain - uses: moonrepo/setup-toolchain@v0 - with: - moon-version: "2.0.0" - auto-install: true - - - name: Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - - name: Smoke test the JSR entry points with Deno - run: moon run cssforge:jsr-smoke -- --require-deno diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7062931..e2746a4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -137,11 +137,6 @@ jobs: moon-version: "2.0.0" auto-install: true - - name: Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - name: Check release metadata consistency run: moon run --force cssforge:version-check diff --git a/.gitignore b/.gitignore index 010f5ca..a5be974 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,4 @@ convert.ts /.cssforge TODO.md .worktrees -.jsr-smoke-* .artifact-build-* diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a05ecf..d76c7fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,10 @@ { - "deno.enable": true, - "deno.lint": true, "editor.formatOnSave": true, "[typescript]": { - "editor.defaultFormatter": "denoland.vscode-deno" + "editor.defaultFormatter": "biomejs.biome" }, "[json]": { - "editor.defaultFormatter": "denoland.vscode-deno" + "editor.defaultFormatter": "biomejs.biome" }, "oxc.enable": false } diff --git a/README.md b/README.md index 3f29e78..e662943 100644 --- a/README.md +++ b/README.md @@ -56,30 +56,20 @@ npx cssforge --mode all # npm The rest of this document writes commands as `cssforge `. -### Alternative installation (Deno and JSR) +### Alternative installation (JSR) CSS Forge is also published to [JSR](https://jsr.io/@hebilicious/cssforge) at the same version -as npm. Use it for Deno projects and for JSR-native imports: +as npm. Use it for JSR-native imports: ```bash -# Deno -deno add jsr:@hebilicious/cssforge - -# npm (10.9 +) or pnpm +# npm npx jsr add @hebilicious/cssforge -pnpm i jsr:@hebilicious/cssforge -``` - -The `jsr:` specifier replaces the package name in imports, and the published CLI entry -point runs directly with Deno: -```bash -deno run -A jsr:@hebilicious/cssforge/cli --mode all +# pnpm 10.9+ +pnpm i jsr:@hebilicious/cssforge ``` -```typescript -import { defineConfig } from "jsr:@hebilicious/cssforge"; -``` +Both keep the package name, so imports stay `import { defineConfig } from "@hebilicious/cssforge";`. ## Contributing Workflow @@ -97,8 +87,7 @@ moon run cssforge:jsr-dry-run ``` `cssforge:smoke-test` packs the package and installs that tarball in clean npm and pnpm -projects, and `cssforge:jsr-smoke` exercises the JSR entry points (with Deno when it is -installed). +projects, and `cssforge:jsr-smoke` runs the JSR entry points with Node. ## Quick Start @@ -272,8 +261,8 @@ The plugin calls the same `generateCSS` implementation as the CLI, so `virtual:c `cssforge --mode css` output byte for byte. The served stylesheet is unlayered, and `@import "virtual:cssforge.css" layer(cssforge)` does not work because a CSS import resolves to a file; set `write` and import the written file with `layer(cssforge)` to keep the tokens in a layer. -The CLI stays the integration path for everything else: Deno and JSR, Style Dictionary, CI steps, and -tools without a bundler. +The CLI stays the integration path for everything else: JSR, Style Dictionary, CI steps, and tools +without a bundler. ## Output Formats diff --git a/packages/cssforge/README.md b/packages/cssforge/README.md index 3f29e78..e662943 100644 --- a/packages/cssforge/README.md +++ b/packages/cssforge/README.md @@ -56,30 +56,20 @@ npx cssforge --mode all # npm The rest of this document writes commands as `cssforge `. -### Alternative installation (Deno and JSR) +### Alternative installation (JSR) CSS Forge is also published to [JSR](https://jsr.io/@hebilicious/cssforge) at the same version -as npm. Use it for Deno projects and for JSR-native imports: +as npm. Use it for JSR-native imports: ```bash -# Deno -deno add jsr:@hebilicious/cssforge - -# npm (10.9 +) or pnpm +# npm npx jsr add @hebilicious/cssforge -pnpm i jsr:@hebilicious/cssforge -``` - -The `jsr:` specifier replaces the package name in imports, and the published CLI entry -point runs directly with Deno: -```bash -deno run -A jsr:@hebilicious/cssforge/cli --mode all +# pnpm 10.9+ +pnpm i jsr:@hebilicious/cssforge ``` -```typescript -import { defineConfig } from "jsr:@hebilicious/cssforge"; -``` +Both keep the package name, so imports stay `import { defineConfig } from "@hebilicious/cssforge";`. ## Contributing Workflow @@ -97,8 +87,7 @@ moon run cssforge:jsr-dry-run ``` `cssforge:smoke-test` packs the package and installs that tarball in clean npm and pnpm -projects, and `cssforge:jsr-smoke` exercises the JSR entry points (with Deno when it is -installed). +projects, and `cssforge:jsr-smoke` runs the JSR entry points with Node. ## Quick Start @@ -272,8 +261,8 @@ The plugin calls the same `generateCSS` implementation as the CLI, so `virtual:c `cssforge --mode css` output byte for byte. The served stylesheet is unlayered, and `@import "virtual:cssforge.css" layer(cssforge)` does not work because a CSS import resolves to a file; set `write` and import the written file with `layer(cssforge)` to keep the tokens in a layer. -The CLI stays the integration path for everything else: Deno and JSR, Style Dictionary, CI steps, and -tools without a bundler. +The CLI stays the integration path for everything else: JSR, Style Dictionary, CI steps, and tools +without a bundler. ## Output Formats diff --git a/packages/cssforge/scripts/jsr-smoke.ts b/packages/cssforge/scripts/jsr-smoke.ts index 1e87009..582dd26 100644 --- a/packages/cssforge/scripts/jsr-smoke.ts +++ b/packages/cssforge/scripts/jsr-smoke.ts @@ -4,15 +4,16 @@ * JSR publishes the TypeScript source declared in `jsr.json`, not `dist`, so * this verifies that those entry points stay publishable and executable: the * declared exports exist, the manifest versions agree, and both the module - * entry and the CLI entry generate output. + * entry and the CLI entry generate output when Node runs those sources. * - * Deno is the real consumer of this channel, so when Deno is installed the same - * checks also run under Deno. Pass `--require-deno` to turn a missing Deno into - * a failure, which is what CI does. + * Node resolves the bare imports of those sources from `node_modules`, so the + * `imports` map that JSR publishes with is checked against the runtime + * dependencies in `package.json` instead. */ import { spawnSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join, matchesGlob, resolve } from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; @@ -21,17 +22,14 @@ type JsrManifest = { name: string; version: string; exports: Record; + imports: Record; publish?: { include?: string[] }; }; type PackageManifest = { name: string; version: string; -}; - -type Runtime = { - name: string; - command: string; + dependencies: Record; }; const packageRoot = resolve(import.meta.dirname, ".."); @@ -52,6 +50,18 @@ const parseJsonFile = (path: string): Record => { return parsed; }; +const readStringRecord = (value: unknown, path: string, field: string) => { + const record = value ?? {}; + check(isRecord(record), `${path} "${field}" must be an object`); + const entries = Object.entries(record); + check( + entries.every((entry) => typeof entry[1] === "string"), + `${path} "${field}" must map names to strings`, + ); + + return Object.fromEntries(entries) as Record; +}; + /** Reads a manifest and validates the fields this script relies on. */ const readManifest = (path: string): PackageManifest => { const parsed = parseJsonFile(path); @@ -60,7 +70,11 @@ const readManifest = (path: string): PackageManifest => { `${path} must declare "name" and "version"`, ); - return { name: parsed.name, version: parsed.version }; + return { + name: parsed.name, + version: parsed.version, + dependencies: readStringRecord(parsed.dependencies, path, "dependencies"), + }; }; /** Reads the JSR manifest and validates the entries it publishes. */ @@ -87,22 +101,23 @@ const readJsrManifest = (path: string): JsrManifest => { name: parsed.name, version: parsed.version, exports: Object.fromEntries(exports), + imports: readStringRecord(parsed.imports, path, "imports"), publish: { include }, }; }; -const run = (runtime: Runtime, args: string[], cwd: string): string => { - const result = spawnSync(runtime.command, args, { +const runEntry = (entry: string, args: string[], cwd: string): string => { + const result = spawnSync(process.execPath, [entry, ...args], { cwd, encoding: "utf8", env: { ...process.env, TEST: undefined, NODE_ENV: undefined }, }); if (result.error) { - throw new Error(`${runtime.name} could not start: ${result.error.message}`); + throw new Error(`node could not start: ${result.error.message}`); } if (result.status !== 0) { throw new Error( - `${runtime.name} ${args.join(" ")} exited with code ${result.status}\n` + + `node ${entry} ${args.join(" ")} exited with code ${result.status}\n` + `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`, ); } @@ -110,23 +125,7 @@ const run = (runtime: Runtime, args: string[], cwd: string): string => { return result.stdout; }; -const runEntry = (runtime: Runtime, entry: string, args: string[], cwd: string) => - run( - runtime, - runtime.name === "deno" - ? ["run", "-A", "--no-lock", entry, ...args] - : [entry, ...args], - cwd, - ); - -const findDeno = (): string | undefined => { - const result = spawnSync("deno", ["--version"], { encoding: "utf8" }); - - return result.status === 0 ? "deno" : undefined; -}; - const main = async (): Promise => { - const requireDeno = process.argv.includes("--require-deno"); const manifest = readManifest(join(packageRoot, "package.json")); const jsr = readJsrManifest(join(packageRoot, "jsr.json")); @@ -159,57 +158,53 @@ const main = async (): Promise => { `${jsr.exports["./cli"]} is not covered by jsr.json publish.include`, ); - const deno = findDeno(); - check(!requireDeno || deno !== undefined, "deno is required but was not found"); - - const runtimes: Runtime[] = [ - { name: "node", command: process.execPath }, - ...(deno ? [{ name: "deno", command: deno }] : []), - ]; + for (const [name, version] of Object.entries(manifest.dependencies)) { + const expected = `npm:${name}@${version}`; + check( + jsr.imports[name] === expected, + `jsr.json imports ${name} as ${jsr.imports[name] ?? "nothing"} but package.json depends on ${expected}`, + ); + } - // The fixtures live inside the package so that Deno resolves the bare npm - // specifiers of the JSR entries from this package manifest. - const workDir = await mkdtemp(join(packageRoot, ".jsr-smoke-")); + const workDir = await mkdtemp(join(tmpdir(), "cssforge-jsr-smoke-")); try { const moduleUrl = pathToFileURL(moduleEntry).href; - for (const runtime of runtimes) { - const configPath = join(workDir, `cssforge.${runtime.name}.config.ts`); - const cssOutput = join(workDir, `output.${runtime.name}.css`); - await writeFile( - configPath, - `import { defineConfig } from "${moduleUrl}"; + const configPath = join(workDir, "cssforge.config.ts"); + const cssOutput = join(workDir, "output.css"); + await writeFile( + configPath, + `import { defineConfig } from "${moduleUrl}"; export default defineConfig({ spacing: { custom: { size: { value: { 2: "0.5rem" } } } }, }); `, - "utf8", - ); - - runEntry( - runtime, - cliEntry, - ["--config", configPath, "--mode", "css", "--css", cssOutput], - workDir, - ); - const css = await readFile(cssOutput, "utf8"); - check( - css.includes("--spacing-size-2: 0.5rem;"), - `the JSR CLI entry did not generate the expected CSS with ${runtime.name}`, - ); - - // JSR ships sources, so the CLI metadata there comes from the - // generated `src/version.ts` rather than from the build. - const reported = runEntry(runtime, cliEntry, ["--version"], workDir).trim(); - check( - reported === manifest.version, - `the JSR CLI entry reports ${reported} but package.json is at ${manifest.version}`, - ); - - const moduleCheck = join(workDir, `module-check.${runtime.name}.mjs`); - await writeFile( - moduleCheck, - `const mod = await import(${JSON.stringify(moduleUrl)}); + "utf8", + ); + + runEntry( + cliEntry, + ["--config", configPath, "--mode", "css", "--css", cssOutput], + workDir, + ); + const css = await readFile(cssOutput, "utf8"); + check( + css.includes("--spacing-size-2: 0.5rem;"), + "the JSR CLI entry did not generate the expected CSS", + ); + + // JSR ships sources, so the CLI metadata there comes from the + // generated `src/version.ts` rather than from the build. + const reported = runEntry(cliEntry, ["--version"], workDir).trim(); + check( + reported === manifest.version, + `the JSR CLI entry reports ${reported} but package.json is at ${manifest.version}`, + ); + + const moduleCheck = join(workDir, "module-check.mjs"); + await writeFile( + moduleCheck, + `const mod = await import(${JSON.stringify(moduleUrl)}); for (const name of ["defineConfig", "generateCSS", "generateStyleDictionaryJSON"]) { if (typeof mod[name] !== "function") { @@ -225,14 +220,11 @@ if (!css.includes("--spacing-size-2")) { throw new Error("the JSR module entry did not generate the expected CSS"); } `, - "utf8", - ); - runEntry(runtime, moduleCheck, [], workDir); - } - - const checked = runtimes.map((runtime) => runtime.name).join(" and "); + "utf8", + ); + runEntry(moduleCheck, [], workDir); console.log( - `jsr smoke test: ${jsr.name}@${jsr.version} entries ${jsr.exports["."]} and ${jsr.exports["./cli"]} verified with ${checked}`, + `jsr smoke test: ${jsr.name}@${jsr.version} entries ${jsr.exports["."]} and ${jsr.exports["./cli"]} verified with node`, ); } finally { await rm(workDir, { recursive: true, force: true }); diff --git a/packages/cssforge/scripts/publish.ts b/packages/cssforge/scripts/publish.ts index 92f4992..ba61479 100644 --- a/packages/cssforge/scripts/publish.ts +++ b/packages/cssforge/scripts/publish.ts @@ -3,7 +3,7 @@ * * npm is the primary channel and is published with trusted publishing (OIDC), * which the release workflow enables through `id-token: write`. JSR stays - * available as the secondary channel for Deno users. + * available as the secondary channel for JSR-native imports. * * A version that is already on the channel is skipped instead of republished, * so a failed or retried release run never fails on an already-published npm @@ -12,7 +12,8 @@ * Usage: node ./scripts/publish.ts [--dry-run] */ import { spawnSync } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import process from "node:process"; @@ -87,9 +88,9 @@ const run = (command: string, args: string[], dryRun: boolean): void => { } }; -const capture = (command: string, args: string[]): string => { +const capture = (command: string, args: string[], cwd = packageRoot): string => { const result = spawnSync(command, args, { - cwd: packageRoot, + cwd, encoding: "utf8", }); if (result.error) { @@ -107,43 +108,70 @@ const capture = (command: string, args: string[]): string => { /** * A published JSR entry point that fails to run is silent, so the release has to - * observe real output. The minimum dependency age is disabled because the - * version was published seconds ago. + * observe real output. Node consumers install JSR packages through its npm + * compatibility registry, as `npx jsr add` does, so the check installs the + * published version from there into an empty project and runs both entries. */ const verifyJsrChannel = (manifest: PackageManifest): void => { - const specifier = `jsr:${manifest.name}@${manifest.version}`; - const help = capture("deno", [ - "run", - "-A", - "--no-lock", - "--minimum-dependency-age=0", - `${specifier}/cli`, - "--help", - ]); - if (!help.includes("USAGE")) { - throw new Error(`${specifier}/cli did not print its usage`); - } - - const moduleCheck = [ - `const mod = await import(${JSON.stringify(specifier)});`, - `const css = mod.generateCSS(mod.defineConfig({ spacing: { custom: { size: { value: { 2: "0.5rem" } } } } }));`, - `if (!css.includes("--spacing-size-2")) {`, - ` throw new Error("the published JSR module entry did not generate the expected CSS");`, - `}`, - `console.log("module entry ok");`, - ].join("\n"); - const result = capture("deno", [ - "eval", - "--no-lock", - "--minimum-dependency-age=0", - moduleCheck, - ]); - if (!result.includes("module entry ok")) { - throw new Error(`${specifier} did not import cleanly: ${result}`); + // `@scope/name` is served as `@jsr/scope__name`. + const npmName = `@jsr/${manifest.name.slice(1).replace("/", "__")}`; + const specifier = `${npmName}@${manifest.version}`; + const projectDir = mkdtempSync(join(tmpdir(), "cssforge-jsr-verify-")); + try { + writeFileSync( + join(projectDir, "package.json"), + '{ "private": true, "type": "module" }\n', + ); + writeFileSync(join(projectDir, ".npmrc"), "@jsr:registry=https://npm.jsr.io\n"); + // Retries must ask the registry again rather than reuse a cached + // package document that predates the publish. + capture( + "npm", + ["install", "--prefer-online", "--no-audit", "--no-fund", specifier], + projectDir, + ); + + const cliPath = capture( + process.execPath, + [ + "--input-type=module", + "--eval", + `import { fileURLToPath } from "node:url"; +console.log(fileURLToPath(import.meta.resolve(${JSON.stringify(`${npmName}/cli`)})));`, + ], + projectDir, + ).trim(); + const help = capture(process.execPath, [cliPath, "--help"], projectDir); + if (!help.includes("USAGE")) { + throw new Error(`${specifier}/cli did not print its usage`); + } + + const moduleCheck = [ + `const mod = await import(${JSON.stringify(npmName)});`, + `const css = mod.generateCSS(mod.defineConfig({ spacing: { custom: { size: { value: { 2: "0.5rem" } } } } }));`, + `if (!css.includes("--spacing-size-2")) {`, + ` throw new Error("the published JSR module entry did not generate the expected CSS");`, + `}`, + `console.log("module entry ok");`, + ].join("\n"); + const result = capture( + process.execPath, + ["--input-type=module", "--eval", moduleCheck], + projectDir, + ); + if (!result.includes("module entry ok")) { + throw new Error(`${specifier} did not import cleanly: ${result}`); + } + } finally { + rmSync(projectDir, { recursive: true, force: true }); } }; -const verifyWithRetry = async (verify: () => void, attempts = 3): Promise => { +/** + * JSR builds the npm compatibility tarball after the publish completes, so the + * new version can take a minute to become installable. + */ +const verifyWithRetry = async (verify: () => void, attempts = 6): Promise => { for (let attempt = 1; attempt <= attempts; attempt++) { try { verify(); @@ -153,9 +181,9 @@ const verifyWithRetry = async (verify: () => void, attempts = 3): Promise throw error; } console.log( - `jsr: verification attempt ${attempt} failed, retrying in 5s: ${(error as Error).message}`, + `jsr: verification attempt ${attempt} failed, retrying in 15s: ${(error as Error).message}`, ); - await new Promise((resolve) => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 15_000)); } } }; diff --git a/packages/cssforge/src/cli.ts b/packages/cssforge/src/cli.ts index 6e14655..bc2ed90 100644 --- a/packages/cssforge/src/cli.ts +++ b/packages/cssforge/src/cli.ts @@ -424,9 +424,9 @@ const mainCommand = defineCommand({ }); const isDirectRun = (): boolean => { - // Deno and recent Node versions report whether this module is the entry - // point. This also covers the JSR channel, where the module URL is remote - // and cannot be mapped to a file path. + // Node 24.2 and newer report whether this module is the entry point. This + // also covers a JSR import whose module URL is remote and cannot be mapped + // to a file path. const isMain = (import.meta as ImportMeta & { main?: boolean }).main; if (typeof isMain === "boolean") { return isMain; diff --git a/packages/cssforge/tests/basic-example.test.ts b/packages/cssforge/tests/basic-example.test.ts index f8d9e28..7dc3311 100644 --- a/packages/cssforge/tests/basic-example.test.ts +++ b/packages/cssforge/tests/basic-example.test.ts @@ -1,9 +1,9 @@ import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; import * as basicExampleModule from "../../../example/basic/cssforge.config.ts"; import type { CSSForgeConfig } from "../src/mod.ts"; import { generateCSS } from "../src/mod.ts"; -import { assert, assertEquals, Deno } from "./vitest-compat.ts"; // `example/basic` is excluded from the Moon workspace, so its stylesheet and // markup are only observable as files. @@ -23,7 +23,7 @@ const consumedProperties = (source: string): Set => const importedStylesheets = (css: string): string[] => [...css.matchAll(/@import\s+["']([^"']+)["']/g)].map((match) => match[1]); -Deno.test("example/basic consumes only custom properties its configuration generates", async () => { +test("example/basic consumes only custom properties its configuration generates", async () => { const declared = declaredProperties(generateCSS(basicExampleConfig)); const consumed = new Set([ ...consumedProperties(await readFile(exampleFile("index.html"), "utf8")), @@ -32,18 +32,17 @@ Deno.test("example/basic consumes only custom properties its configuration gener const undeclared = [...consumed].filter((name) => !declared.has(name)).sort(); - assertEquals( + expect( undeclared, - [], `example/basic consumes custom properties its configuration never generates: ${undeclared.join(", ")}`, - ); + ).toEqual([]); }); -Deno.test("example/basic imports the generated stylesheet from the CLI default output path", async () => { +test("example/basic imports the generated stylesheet from the CLI default output path", async () => { const imports = importedStylesheets(await readFile(exampleFile("main.css"), "utf8")); - assert( + expect( imports.includes("./.cssforge/output.css"), `example/basic/main.css must import "./.cssforge/output.css", got: ${imports.join(", ")}`, - ); + ).toBeTruthy(); }); diff --git a/packages/cssforge/tests/build-mode.test.ts b/packages/cssforge/tests/build-mode.test.ts index b0bec2e..d8a58ec 100644 --- a/packages/cssforge/tests/build-mode.test.ts +++ b/packages/cssforge/tests/build-mode.test.ts @@ -2,8 +2,8 @@ import { existsSync } from "node:fs"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { expect, test } from "vitest"; import { build } from "../src/cli.ts"; -import { assertEquals, Deno } from "./vitest-compat.ts"; const configSource = `export default { spacing: { @@ -39,7 +39,7 @@ const callWithMode = (mode: unknown, paths: BuildPaths) => styleDictionaryOutput: paths.styleDictionary, }); -Deno.test("build - rejects an unsupported output mode for JavaScript callers", async () => { +test("build - rejects an unsupported output mode for JavaScript callers", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-build-invalid-mode-")); try { @@ -49,23 +49,23 @@ Deno.test("build - rejects an unsupported output mode for JavaScript callers", a const result = await callWithMode("typo", paths); - assertEquals(result.success, false); + expect(result.success).toEqual(false); const message = result.error instanceof Error ? result.error.message : String(result.error); - assertEquals(message.includes("typo"), true); + expect(message.includes("typo")).toEqual(true); for (const mode of ["css", "json", "ts", "style-dictionary", "all"]) { - assertEquals(message.includes(mode), true); + expect(message.includes(mode)).toEqual(true); } - assertEquals(await readFile(paths.css, "utf8"), "/* pre-existing css */"); - assertEquals(existsSync(paths.json), false); - assertEquals(existsSync(paths.ts), false); - assertEquals(existsSync(paths.styleDictionary), false); + expect(await readFile(paths.css, "utf8")).toEqual("/* pre-existing css */"); + expect(existsSync(paths.json)).toEqual(false); + expect(existsSync(paths.ts)).toEqual(false); + expect(existsSync(paths.styleDictionary)).toEqual(false); } finally { await rm(tempDir, { recursive: true, force: true }); } }); -Deno.test("build - accepts every supported output mode", async () => { +test("build - accepts every supported output mode", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-build-valid-modes-")); try { @@ -83,15 +83,13 @@ Deno.test("build - accepts every supported output mode", async () => { const result = await callWithMode(mode, paths); - assertEquals(result.success, true, `mode ${mode} should succeed`); + expect(result.success, `mode ${mode} should succeed`).toEqual(true); for (const [name, path] of Object.entries(paths)) { if (name === "config") { continue; } - assertEquals( - existsSync(path), + expect(existsSync(path), `mode ${mode} output ${name}`).toEqual( produced.includes(name), - `mode ${mode} output ${name}`, ); } } diff --git a/packages/cssforge/tests/cli-color-formats.test.ts b/packages/cssforge/tests/cli-color-formats.test.ts index 1124b79..9d18826 100644 --- a/packages/cssforge/tests/cli-color-formats.test.ts +++ b/packages/cssforge/tests/cli-color-formats.test.ts @@ -3,9 +3,9 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; import { build, parseColorFormats } from "../src/cli.ts"; -import { childEnv } from "./helpers.ts"; -import { assert, assertEquals, assertThrows, Deno } from "./vitest-compat.ts"; +import { childEnv, thrownError } from "./helpers.ts"; const cliPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); @@ -51,23 +51,23 @@ const buildWithFormats = (paths: BuildPaths, colorFormats: unknown) => colorFormats: colorFormats as never, }); -Deno.test("parseColorFormats - reads a comma separated list and drops duplicates", () => { - assertEquals(parseColorFormats(undefined), undefined); - assertEquals(parseColorFormats(""), []); - assertEquals(parseColorFormats("hex, rgb"), ["hex", "rgb"]); - assertEquals(parseColorFormats("rgb,hex,rgb"), ["rgb", "hex"]); +test("parseColorFormats - reads a comma separated list and drops duplicates", () => { + expect(parseColorFormats(undefined)).toEqual(undefined); + expect(parseColorFormats("")).toEqual([]); + expect(parseColorFormats("hex, rgb")).toEqual(["hex", "rgb"]); + expect(parseColorFormats("rgb,hex,rgb")).toEqual(["rgb", "hex"]); }); -Deno.test("parseColorFormats - rejects a format the generator cannot emit", () => { - const error = assertThrows(() => parseColorFormats("hex,hsl")); +test("parseColorFormats - rejects a format the generator cannot emit", () => { + const error = thrownError(() => parseColorFormats("hex,hsl")); - assert( + expect( error.message.includes("hsl") && error.message.includes("hex"), `Expected the error to name the rejected value and the accepted formats. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("build - --color-formats adds the formats to every output", async () => { +test("build - --color-formats adds the formats to every output", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-color-formats-")); try { @@ -76,20 +76,20 @@ Deno.test("build - --color-formats adds the formats to every output", async () = const result = await buildWithFormats(paths, parseColorFormats("hex,rgb")); - assertEquals(result.success, true); + expect(result.success).toEqual(true); const css = await readFile(paths.css, "utf8"); const json = JSON.parse(await readFile(paths.json, "utf8")); const ts = await readFile(paths.ts, "utf8"); const styleDictionary = JSON.parse(await readFile(paths.styleDictionary, "utf8")); - assertEquals(css.includes("--palette-coral-100: #ff7f50;"), true); - assertEquals(json.palette.coral["100"].color, { + expect(css.includes("--palette-coral-100: #ff7f50;")).toEqual(true); + expect(json.palette.coral["100"].color).toEqual({ hex: { string: "#ff7f50" }, rgb: { string: "rgb(255 127 80)" }, }); - assertEquals(ts.includes('"rgb": {'), true); - assertEquals(styleDictionary.palette.coral["100"].$color, { + expect(ts.includes('"rgb": {')).toEqual(true); + expect(styleDictionary.palette.coral["100"].$color).toEqual({ hex: { string: "#ff7f50" }, rgb: { string: "rgb(255 127 80)" }, }); @@ -98,7 +98,7 @@ Deno.test("build - --color-formats adds the formats to every output", async () = } }); -Deno.test("build - rejects an unknown color format for JavaScript callers", async () => { +test("build - rejects an unknown color format for JavaScript callers", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-color-formats-invalid-")); try { @@ -107,16 +107,16 @@ Deno.test("build - rejects an unknown color format for JavaScript callers", asyn const result = await buildWithFormats(paths, ["hsl"]); - assertEquals(result.success, false); + expect(result.success).toEqual(false); const message = result.error instanceof Error ? result.error.message : String(result.error); - assertEquals(message.includes("hsl"), true); + expect(message.includes("hsl")).toEqual(true); } finally { await rm(tempDir, { recursive: true, force: true }); } }); -Deno.test("cli - --color-formats writes the formats through the CLI entry", async () => { +test("cli - --color-formats writes the formats through the CLI entry", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-color-formats-cli-")); try { @@ -137,15 +137,15 @@ Deno.test("cli - --color-formats writes the formats through the CLI entry", asyn tempDir, ); - assertEquals(result.status, 0, result.stderr); + expect(result.status, result.stderr).toEqual(0); const css = await readFile(paths.css, "utf8"); - assertEquals(css.includes("--palette-coral-100: #ff7f50;"), true); + expect(css.includes("--palette-coral-100: #ff7f50;")).toEqual(true); } finally { await rm(tempDir, { recursive: true, force: true }); } }); -Deno.test("cli - --color-formats reports an unknown format", async () => { +test("cli - --color-formats reports an unknown format", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-color-formats-cli-invalid-")); try { @@ -166,11 +166,9 @@ Deno.test("cli - --color-formats reports an unknown format", async () => { tempDir, ); - assertEquals(result.status, 1); - assertEquals( - result.stderr.includes("Invalid color format: hsl"), + expect(result.status).toEqual(1); + expect(result.stderr.includes("Invalid color format: hsl"), result.stderr).toEqual( true, - result.stderr, ); } finally { await rm(tempDir, { recursive: true, force: true }); diff --git a/packages/cssforge/tests/cli-diagnostics.test.ts b/packages/cssforge/tests/cli-diagnostics.test.ts index 505d834..54e42af 100644 --- a/packages/cssforge/tests/cli-diagnostics.test.ts +++ b/packages/cssforge/tests/cli-diagnostics.test.ts @@ -3,8 +3,8 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; import { childEnv } from "./helpers.ts"; -import { assert, assertEquals, Deno } from "./vitest-compat.ts"; const cliPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); @@ -27,7 +27,7 @@ const configSource = `export default { }, };`; -Deno.test("cli - prints a build warning to stderr and still writes the output", async () => { +test("cli - prints a build warning to stderr and still writes the output", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-diagnostics-cli-")); try { @@ -41,16 +41,16 @@ Deno.test("cli - prints a build warning to stderr and still writes the output", { cwd: tempDir, encoding: "utf8", env: childEnv }, ); - assertEquals(result.status, 0, result.stderr); + expect(result.status, result.stderr).toEqual(0); const warnings = result.stderr .split("\n") .filter((line) => line.startsWith("cssforge: warning: ")); - assertEquals(warnings.length, 1, result.stderr); - assert(warnings[0]?.includes("typography_fluid.body@m"), warnings[0]); - assert( + expect(warnings.length, result.stderr).toEqual(1); + expect(warnings[0]?.includes("typography_fluid.body@m"), warnings[0]).toBeTruthy(); + expect( (await readFile(cssPath, "utf8")).includes("--typography_fluid-body-m:"), "Expected the CSS output to be written", - ); + ).toBeTruthy(); } finally { await rm(tempDir, { recursive: true, force: true }); } diff --git a/packages/cssforge/tests/cli-entry.test.ts b/packages/cssforge/tests/cli-entry.test.ts index 018677a..8b45c34 100644 --- a/packages/cssforge/tests/cli-entry.test.ts +++ b/packages/cssforge/tests/cli-entry.test.ts @@ -4,10 +4,10 @@ import { mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; import { childEnv } from "./helpers.ts"; -import { assertEquals, Deno } from "./vitest-compat.ts"; -Deno.test("cli - runs when the entry point is invoked through a symlink", async () => { +test("cli - runs when the entry point is invoked through a symlink", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-cli-entry-")); try { @@ -38,8 +38,8 @@ Deno.test("cli - runs when the entry point is invoked through a symlink", async env: childEnv, }); - assertEquals(help.status, 0); - assertEquals(help.stdout.includes("USAGE"), true); + expect(help.status).toEqual(0); + expect(help.stdout.includes("USAGE")).toEqual(true); const generate = spawnSync( process.execPath, @@ -47,10 +47,12 @@ Deno.test("cli - runs when the entry point is invoked through a symlink", async { cwd: tempDir, encoding: "utf8", env: childEnv }, ); - assertEquals(generate.status, 0); - assertEquals(generate.stderr, ""); - assertEquals(existsSync(cssOutput), true); - assertEquals((await readFile(cssOutput, "utf8")).includes("--spacing-size-2"), true); + expect(generate.status).toEqual(0); + expect(generate.stderr).toEqual(""); + expect(existsSync(cssOutput)).toEqual(true); + expect((await readFile(cssOutput, "utf8")).includes("--spacing-size-2")).toEqual( + true, + ); } finally { await rm(tempDir, { recursive: true, force: true }); } diff --git a/packages/cssforge/tests/cli-mode.test.ts b/packages/cssforge/tests/cli-mode.test.ts index 8910589..5fab19e 100644 --- a/packages/cssforge/tests/cli-mode.test.ts +++ b/packages/cssforge/tests/cli-mode.test.ts @@ -4,7 +4,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { assertEquals, Deno } from "./vitest-compat.ts"; +import { expect, test } from "vitest"; const cliPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); @@ -46,7 +46,7 @@ const outputArgs = (paths: ReturnType) => [ paths.styleDictionary, ]; -Deno.test("cli - each supported mode writes only its declared outputs", async () => { +test("cli - each supported mode writes only its declared outputs", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-mode-outputs-")); try { @@ -68,13 +68,11 @@ Deno.test("cli - each supported mode writes only its declared outputs", async () tempDir, ); - assertEquals(result.status, 0, `mode ${mode} should exit 0`); - assertEquals(result.stderr, ""); + expect(result.status, `mode ${mode} should exit 0`).toEqual(0); + expect(result.stderr).toEqual(""); for (const [name, path] of Object.entries(paths)) { - assertEquals( - existsSync(path), + expect(existsSync(path), `mode ${mode} output ${name}`).toEqual( (produced as readonly string[]).includes(name), - `mode ${mode} output ${name}`, ); } for (const path of Object.values(paths)) { @@ -86,7 +84,7 @@ Deno.test("cli - each supported mode writes only its declared outputs", async () } }); -Deno.test("cli - omitted mode defaults to all", async () => { +test("cli - omitted mode defaults to all", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-mode-default-")); try { @@ -96,17 +94,17 @@ Deno.test("cli - omitted mode defaults to all", async () => { const result = runCli(["--config", configPath, ...outputArgs(paths)], tempDir); - assertEquals(result.status, 0); - assertEquals(result.stderr, ""); + expect(result.status).toEqual(0); + expect(result.stderr).toEqual(""); for (const path of Object.values(paths)) { - assertEquals(existsSync(path), true, `${path} should exist`); + expect(existsSync(path), `${path} should exist`).toEqual(true); } } finally { await rm(tempDir, { recursive: true, force: true }); } }); -Deno.test("cli - the -m alias selects the output mode", async () => { +test("cli - the -m alias selects the output mode", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-mode-alias-")); try { @@ -119,17 +117,17 @@ Deno.test("cli - the -m alias selects the output mode", async () => { tempDir, ); - assertEquals(result.status, 0); - assertEquals(result.stderr, ""); - assertEquals(existsSync(paths.json), true); - assertEquals(existsSync(paths.css), false); - assertEquals(existsSync(paths.ts), false); + expect(result.status).toEqual(0); + expect(result.stderr).toEqual(""); + expect(existsSync(paths.json)).toEqual(true); + expect(existsSync(paths.css)).toEqual(false); + expect(existsSync(paths.ts)).toEqual(false); } finally { await rm(tempDir, { recursive: true, force: true }); } }); -Deno.test("cli - an unsupported mode fails without touching existing outputs", async () => { +test("cli - an unsupported mode fails without touching existing outputs", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-mode-invalid-")); try { @@ -144,22 +142,22 @@ Deno.test("cli - an unsupported mode fails without touching existing outputs", a tempDir, ); - assertEquals(result.status, 1); - assertEquals(result.stderr.includes("Invalid output mode"), true); - assertEquals(result.stderr.includes("typo"), true); + expect(result.status).toEqual(1); + expect(result.stderr.includes("Invalid output mode")).toEqual(true); + expect(result.stderr.includes("typo")).toEqual(true); for (const mode of ["css", "json", "ts", "style-dictionary", "all"]) { - assertEquals(result.stderr.includes(mode), true, `stderr should name ${mode}`); + expect(result.stderr.includes(mode), `stderr should name ${mode}`).toEqual(true); } - assertEquals(await readFile(paths.css, "utf8"), "/* pre-existing css */"); - assertEquals(await readFile(paths.json, "utf8"), "/* pre-existing json */"); - assertEquals(existsSync(paths.ts), false); - assertEquals(existsSync(paths.styleDictionary), false); + expect(await readFile(paths.css, "utf8")).toEqual("/* pre-existing css */"); + expect(await readFile(paths.json, "utf8")).toEqual("/* pre-existing json */"); + expect(existsSync(paths.ts)).toEqual(false); + expect(existsSync(paths.styleDictionary)).toEqual(false); } finally { await rm(tempDir, { recursive: true, force: true }); } }); -Deno.test("cli - watch mode rejects an unsupported mode without writing outputs", async () => { +test("cli - watch mode rejects an unsupported mode without writing outputs", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-mode-watch-invalid-")); try { @@ -181,10 +179,10 @@ Deno.test("cli - watch mode rejects an unsupported mode without writing outputs" child.on("close", resolve); }); - assertEquals(exitCode, 0, `watch() rejected: ${stderr}`); - assertEquals(stderr.includes("Invalid output mode"), true); - assertEquals(stdout.includes("Watching"), false); - assertEquals(existsSync(paths.css), false); + expect(exitCode, `watch() rejected: ${stderr}`).toEqual(0); + expect(stderr.includes("Invalid output mode")).toEqual(true); + expect(stdout.includes("Watching")).toEqual(false); + expect(existsSync(paths.css)).toEqual(false); } finally { await rm(tempDir, { recursive: true, force: true }); } diff --git a/packages/cssforge/tests/cli-output-paths.test.ts b/packages/cssforge/tests/cli-output-paths.test.ts index 2a99731..fdbe1cd 100644 --- a/packages/cssforge/tests/cli-output-paths.test.ts +++ b/packages/cssforge/tests/cli-output-paths.test.ts @@ -4,9 +4,9 @@ import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve, win32 } from "node:path"; import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; import { writeFileRecursive } from "../src/cli.ts"; import { childEnv } from "./helpers.ts"; -import { assertEquals, Deno } from "./vitest-compat.ts"; const cssForgeConfig = `export default { spacing: { @@ -57,12 +57,12 @@ const inTempCwd = async (run: (dir: string) => Promise) => { /** Asserts the path is a file, never a directory, with exactly `expected` content. */ const assertFileWithContents = async (path: string, expected: string) => { const stats = await stat(path); - assertEquals(stats.isDirectory(), false, `${path} must not be a directory`); - assertEquals(stats.isFile(), true, `${path} must be a file`); - assertEquals(await readFile(path, "utf8"), expected); + expect(stats.isDirectory(), `${path} must not be a directory`).toEqual(false); + expect(stats.isFile(), `${path} must be a file`).toEqual(true); + expect(await readFile(path, "utf8")).toEqual(expected); }; -Deno.test("writeFileRecursive - creates the parent directory and writes the file for every path shape", async () => { +test("writeFileRecursive - creates the parent directory and writes the file for every path shape", async () => { await inTempCwd(async (dir) => { for (const [shape, outputDir] of outputDirectoryShapes(dir)) { const outputPath = join(outputDir, "output.css"); @@ -73,12 +73,12 @@ Deno.test("writeFileRecursive - creates the parent directory and writes the file }); }); -Deno.test("writeFileRecursive - resolves a Windows output path's parent with the win32 dirname", async () => { +test("writeFileRecursive - resolves a Windows output path's parent with the win32 dirname", async () => { const windowsOutput = String.raw`C:\project\.cssforge\output.css`; const windowsParent = String.raw`C:\project\.cssforge`; // The platform-aware primitive the helper is required to depend on. - assertEquals(win32.dirname(windowsOutput), windowsParent); + expect(win32.dirname(windowsOutput)).toEqual(windowsParent); await inTempCwd(async (dir) => { await writeFileRecursive(windowsOutput, "css{}", win32.dirname); @@ -87,11 +87,10 @@ Deno.test("writeFileRecursive - resolves a Windows output path's parent with the // path is a literal relative path. What this observes is the parent the // helper chose: the containing directory, never the output file path. const parentStats = await stat(join(dir, windowsParent)); - assertEquals( + expect( parentStats.isDirectory(), - true, `${windowsParent} must be created as a directory`, - ); + ).toEqual(true); await assertFileWithContents(join(dir, windowsOutput), "css{}"); }); }); @@ -109,12 +108,11 @@ const cliEntries = [ ]; for (const { label, entry } of cliEntries) { - Deno.test(`cli - writes every output kind for every path shape through the ${label} entry`, async () => { - assertEquals( + test(`cli - writes every output kind for every path shape through the ${label} entry`, async () => { + expect( existsSync(entry), - true, `${entry} is missing; run \`moon run cssforge:build\` before running this suite directly`, - ); + ).toEqual(true); const dir = await mkdtemp(join(tmpdir(), "cssforge-cli-paths-")); @@ -139,26 +137,22 @@ for (const { label, entry } of cliEntries) { { cwd: dir, encoding: "utf8", env: childEnv }, ); - assertEquals( + expect( generate.status, - 0, `${label} failed for ${shape}: ${generate.stderr}`, - ); + ).toEqual(0); for (const [flag, outputPath] of outputs) { const target = resolve(dir, outputPath); const stats = await stat(target); - assertEquals( - stats.isDirectory(), + expect(stats.isDirectory(), `${flag} wrote a directory at ${target}`).toEqual( false, - `${flag} wrote a directory at ${target}`, ); - assertEquals(stats.isFile(), true, `${flag} must write a file at ${target}`); - assertEquals( + expect(stats.isFile(), `${flag} must write a file at ${target}`).toEqual(true); + expect( (await readFile(target, "utf8")).includes("--spacing-size-2"), - true, `${flag} must write the generated token to ${target}`, - ); + ).toEqual(true); } } } finally { diff --git a/packages/cssforge/tests/color-formats.test.ts b/packages/cssforge/tests/color-formats.test.ts index aa00750..97ca83c 100644 --- a/packages/cssforge/tests/color-formats.test.ts +++ b/packages/cssforge/tests/color-formats.test.ts @@ -1,8 +1,8 @@ import Color from "colorjs.io"; +import { expect, test } from "vitest"; import { generateJSON, generateTS } from "../src/generator.ts"; import type { CSSForgeConfig } from "../src/mod.ts"; import { defineConfig, generateCSS, generateStyleDictionaryJSON } from "../src/mod.ts"; -import { assert, assertEquals, assertThrows, Deno } from "./vitest-compat.ts"; /** The value declared for `key` inside the generated `@supports` block. */ const declaredValue = (css: string, key: string): string => { @@ -18,7 +18,7 @@ const declaredValue = (css: string, key: string): string => { const tokenColor = (config: Partial) => JSON.parse(generateJSON(config)).palette.coral["100"].color; -Deno.test("generateCSS - a format emits its CSS value under @supports", () => { +test("generateCSS - a format emits its CSS value under @supports", () => { const config = defineConfig({ colors: { palette: { @@ -32,8 +32,7 @@ Deno.test("generateCSS - a format emits its CSS value under @supports", () => { }, }); - assertEquals( - generateCSS(config), + expect(generateCSS(config)).toEqual( [ "/*____ CSSForge ____*/", ":root {", @@ -52,7 +51,7 @@ Deno.test("generateCSS - a format emits its CSS value under @supports", () => { ); }); -Deno.test("generateJSON - hex produces the string, digits and number outputs", () => { +test("generateJSON - hex produces the string, digits and number outputs", () => { const config = defineConfig({ colors: { palette: { @@ -64,12 +63,12 @@ Deno.test("generateJSON - hex produces the string, digits and number outputs", ( }, }); - assertEquals(tokenColor(config), { + expect(tokenColor(config)).toEqual({ hex: { string: "#ff7f50", digits: "ff7f50", number: 16744272 }, }); }); -Deno.test("generateJSON - rgb produces the string and array outputs", () => { +test("generateJSON - rgb produces the string and array outputs", () => { const config = defineConfig({ colors: { palette: { @@ -79,12 +78,12 @@ Deno.test("generateJSON - rgb produces the string and array outputs", () => { }, }); - assertEquals(tokenColor(config), { + expect(tokenColor(config)).toEqual({ rgb: { string: "rgb(255 127 80)", array: [255, 127, 80] }, }); }); -Deno.test("generateJSON - a color with alpha carries it in every output", () => { +test("generateJSON - a color with alpha carries it in every output", () => { const config = defineConfig({ colors: { palette: { @@ -101,13 +100,13 @@ Deno.test("generateJSON - a color with alpha carries it in every output", () => }, }); - assertEquals(tokenColor(config), { + expect(tokenColor(config)).toEqual({ hex: { string: "#0000001f", digits: "0000001f", number: 31 }, rgb: { string: "rgb(0 0 0 / 0.12)", array: [0, 0, 0, 0.12] }, }); }); -Deno.test("generateCSS - fallback selects the format of the CSS declaration", () => { +test("generateCSS - fallback selects the format of the CSS declaration", () => { const config = defineConfig({ colors: { palette: { @@ -121,14 +120,14 @@ Deno.test("generateCSS - fallback selects the format of the CSS declaration", () const css = generateCSS(config); - assertEquals(declaredValue(css, "--palette-coral-100"), "rgb(255 127 80)"); - assertEquals(tokenColor(config), { + expect(declaredValue(css, "--palette-coral-100")).toEqual("rgb(255 127 80)"); + expect(tokenColor(config)).toEqual({ hex: { string: "#ff7f50" }, rgb: { string: "rgb(255 127 80)" }, }); }); -Deno.test("generateCSS - fallback false keeps the formats out of the CSS", () => { +test("generateCSS - fallback false keeps the formats out of the CSS", () => { const config = defineConfig({ colors: { palette: { @@ -140,11 +139,11 @@ Deno.test("generateCSS - fallback false keeps the formats out of the CSS", () => const css = generateCSS(config); - assertEquals(css.includes("@supports"), false); - assertEquals(tokenColor(config), { hex: { string: "#ff7f50" } }); + expect(css.includes("@supports")).toEqual(false); + expect(tokenColor(config)).toEqual({ hex: { string: "#ff7f50" } }); }); -Deno.test("generateJSON - a format generates at the alpha it sets", () => { +test("generateJSON - a format generates at the alpha it sets", () => { const config = defineConfig({ colors: { palette: { @@ -165,14 +164,14 @@ Deno.test("generateJSON - a format generates at the alpha it sets", () => { // The value keeps the alpha the color carries, and only the format that // asked for an alpha is generated at that opacity. - assertEquals(token.value, "oklch(0% 0 none)"); - assertEquals(token.color, { + expect(token.value).toEqual("oklch(0% 0 none)"); + expect(token.color).toEqual({ hex: { string: "#00000080", digits: "00000080" }, rgb: { string: "rgb(0 0 0)", array: [0, 0, 0] }, }); }); -Deno.test("generateCSS - the declaration uses the alpha of the fallback format", () => { +test("generateCSS - the declaration uses the alpha of the fallback format", () => { const config = defineConfig({ colors: { palette: { @@ -187,10 +186,10 @@ Deno.test("generateCSS - the declaration uses the alpha of the fallback format", }, }); - assertEquals(declaredValue(generateCSS(config), "--palette-coral-100"), "#00000080"); + expect(declaredValue(generateCSS(config), "--palette-coral-100")).toEqual("#00000080"); }); -Deno.test("generateJSON - alpha false generates no alpha channel", () => { +test("generateJSON - alpha false generates no alpha channel", () => { const config = defineConfig({ colors: { palette: { @@ -207,13 +206,13 @@ Deno.test("generateJSON - alpha false generates no alpha channel", () => { }, }); - assertEquals(tokenColor(config), { + expect(tokenColor(config)).toEqual({ hex: { string: "#ff7f50" }, rgb: { array: [255, 127, 80] }, }); }); -Deno.test("generateJSON - a color merges its formats into the palette's", () => { +test("generateJSON - a color merges its formats into the palette's", () => { const config = defineConfig({ colors: { palette: { @@ -235,14 +234,14 @@ Deno.test("generateJSON - a color merges its formats into the palette's", () => // The palette's rgb survives, the color adds hex, and the color's // `fallback: false` still wins over the palette's choice. - assertEquals(tokenColor(config), { + expect(tokenColor(config)).toEqual({ rgb: { string: "rgb(255 127 80)" }, hex: { number: 0xff7f5080 }, }); - assertEquals(generateCSS(config).includes("@supports"), false); + expect(generateCSS(config).includes("@supports")).toEqual(false); }); -Deno.test("generateJSON - a format set to false removes the inherited format", () => { +test("generateJSON - a format set to false removes the inherited format", () => { const config = defineConfig({ colors: { palette: { @@ -260,14 +259,14 @@ Deno.test("generateJSON - a format set to false removes the inherited format", ( const tokens = JSON.parse(generateJSON(config)).palette; - assertEquals(tokens.coral["100"].color, { rgb: { string: "rgb(255 127 80)" } }); - assertEquals(tokens.plain["100"].color, { + expect(tokens.coral["100"].color).toEqual({ rgb: { string: "rgb(255 127 80)" } }); + expect(tokens.plain["100"].color).toEqual({ hex: { string: "#ff7f50" }, rgb: { string: "rgb(255 127 80)" }, }); }); -Deno.test("generateJSON - a format set to false at the palette level is not generated", () => { +test("generateJSON - a format set to false at the palette level is not generated", () => { const config = defineConfig({ colors: { palette: { @@ -277,10 +276,10 @@ Deno.test("generateJSON - a format set to false at the palette level is not gene }, }); - assertEquals(tokenColor(config), { rgb: { string: "rgb(255 127 80)" } }); + expect(tokenColor(config)).toEqual({ rgb: { string: "rgb(255 127 80)" } }); }); -Deno.test("generateCSS - the declaration defaults to the first format with a CSS value", () => { +test("generateCSS - the declaration defaults to the first format with a CSS value", () => { // `hex` produces no CSS value here, so the declaration has to come from // `rgb` instead of failing on the first generated format. const config = defineConfig({ @@ -292,13 +291,12 @@ Deno.test("generateCSS - the declaration defaults to the first format with a CSS }, }); - assertEquals( - declaredValue(generateCSS(config), "--palette-coral-100"), + expect(declaredValue(generateCSS(config), "--palette-coral-100")).toEqual( "rgb(255 127 80)", ); }); -Deno.test("generateCSS - formats without a CSS value emit no declaration", () => { +test("generateCSS - formats without a CSS value emit no declaration", () => { const config = defineConfig({ colors: { palette: { @@ -310,11 +308,11 @@ Deno.test("generateCSS - formats without a CSS value emit no declaration", () => const css = generateCSS(config); - assertEquals(css.includes("@supports"), false); - assertEquals(tokenColor(config), { hex: { number: 16744272 } }); + expect(css.includes("@supports")).toEqual(false); + expect(tokenColor(config)).toEqual({ hex: { number: 16744272 } }); }); -Deno.test("generateCSS - the colorFormats option appends a default format", () => { +test("generateCSS - the colorFormats option appends a default format", () => { const config = defineConfig({ colors: { palette: { @@ -325,19 +323,18 @@ Deno.test("generateCSS - the colorFormats option appends a default format", () = const css = generateCSS(config, { colorFormats: ["rgb"] }); - assertEquals(declaredValue(css, "--palette-coral-100"), "rgb(255 127 80)"); - assertEquals( + expect(declaredValue(css, "--palette-coral-100")).toEqual("rgb(255 127 80)"); + expect( JSON.parse(generateJSON(config, { colorFormats: ["hex", "rgb"] })).palette.coral[ "100" ].color, - { - hex: { string: "#ff7f50" }, - rgb: { string: "rgb(255 127 80)" }, - }, - ); + ).toEqual({ + hex: { string: "#ff7f50" }, + rgb: { string: "rgb(255 127 80)" }, + }); }); -Deno.test("generateCSS - the declaration mirrors the color selector and at-rule", () => { +test("generateCSS - the declaration mirrors the color selector and at-rule", () => { const config = defineConfig({ colors: { palette: { @@ -362,7 +359,7 @@ Deno.test("generateCSS - the declaration mirrors the color selector and at-rule" const css = generateCSS(config); - assert( + expect( css.endsWith( [ "@media (prefers-color-scheme: dark) {", @@ -390,10 +387,10 @@ Deno.test("generateCSS - the declaration mirrors the color selector and at-rule" ].join("\n"), ), `Expected the fallback blocks at the end of:\n${css}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - a palette color without variants emits no color block", () => { +test("generateCSS - a palette color without variants emits no color block", () => { const config = defineConfig({ colors: { palette: { @@ -403,10 +400,10 @@ Deno.test("generateCSS - a palette color without variants emits no color block", }, }); - assertEquals(generateCSS(config).includes("@supports"), false); + expect(generateCSS(config).includes("@supports")).toEqual(false); }); -Deno.test("generateCSS - an alpha percentage keeps its decimals", () => { +test("generateCSS - an alpha percentage keeps its decimals", () => { // 0.549 * 100 is not exact in binary, and the value is what users compare. const config = defineConfig({ colors: { @@ -419,14 +416,13 @@ Deno.test("generateCSS - an alpha percentage keeps its decimals", () => { const css = generateCSS(config); - assertEquals( + expect( css.includes("--palette-soft-100: oklch(15.325% 0.0149 284.50542 / 54.9%);"), - true, - ); - assertEquals(declaredValue(css, "--palette-soft-100"), "#0b0b128c"); + ).toEqual(true); + expect(declaredValue(css, "--palette-soft-100")).toEqual("#0b0b128c"); }); -Deno.test("generateCSS - a wide gamut color uses the CSS gamut mapped sRGB value", () => { +test("generateCSS - a wide gamut color uses the CSS gamut mapped sRGB value", () => { // `oklch(70% 0.4 20)` cannot be shown in sRGB: its conversion is // `rgb(336 -117 10)`, and clipping each channel would give `#ff000a`. The // declaration keeps the authored hue instead, which is how a browser maps @@ -442,11 +438,11 @@ Deno.test("generateCSS - a wide gamut color uses the CSS gamut mapped sRGB value const declared = declaredValue(generateCSS(config), "--palette-vivid-100"); - assertEquals(declared === "#ff000a", false); - assertEquals(new Color(declared).inGamut("srgb", { epsilon: 0 }), true); + expect(declared === "#ff000a").toEqual(false); + expect(new Color(declared).inGamut("srgb", { epsilon: 0 })).toEqual(true); }); -Deno.test("generateJSON - an out-of-sRGB color is flagged as gamut mapped", () => { +test("generateJSON - an out-of-sRGB color is flagged as gamut mapped", () => { const config = defineConfig({ colors: { palette: { @@ -462,28 +458,27 @@ Deno.test("generateJSON - an out-of-sRGB color is flagged as gamut mapped", () = const tokens = JSON.parse(generateJSON(config)).palette; const styleDictionary = JSON.parse(generateStyleDictionaryJSON(config)).palette; - assertEquals(tokens.vivid["100"].gamutMapped, true); - assertEquals(tokens.vivid["100"].color.hex.string, "#ff5464"); - assertEquals(tokens.plain["100"].gamutMapped, undefined); - assertEquals(styleDictionary.vivid["100"].$gamutMapped, true); - assertEquals(styleDictionary.vivid["100"].attributes.gamutMapped, true); - assertEquals(styleDictionary.plain["100"].$gamutMapped, undefined); + expect(tokens.vivid["100"].gamutMapped).toEqual(true); + expect(tokens.vivid["100"].color.hex.string).toEqual("#ff5464"); + expect(tokens.plain["100"].gamutMapped).toEqual(undefined); + expect(styleDictionary.vivid["100"].$gamutMapped).toEqual(true); + expect(styleDictionary.vivid["100"].attributes.gamutMapped).toEqual(true); + expect(styleDictionary.plain["100"].$gamutMapped).toEqual(undefined); }); -Deno.test("generateJSON - a color with no sRGB format is not flagged", () => { +test("generateJSON - a color with no sRGB format is not flagged", () => { const config = defineConfig({ colors: { palette: { value: { vivid: { 100: { oklch: "oklch(70% 0.4 20)" } } } }, }, }); - assertEquals( - JSON.parse(generateJSON(config)).palette.vivid["100"].gamutMapped, + expect(JSON.parse(generateJSON(config)).palette.vivid["100"].gamutMapped).toEqual( undefined, ); }); -Deno.test("generateCSS - a later theme declaration wins over a format", () => { +test("generateCSS - a later theme declaration wins over a format", () => { const config = defineConfig({ colors: { palette: { @@ -509,14 +504,17 @@ Deno.test("generateCSS - a later theme declaration wins over a format", () => { const shim = css.indexOf("@supports not (color: oklch(0% 0 0))"); const theme = css.indexOf(".dark {"); - assert( + expect( declaration >= 0 && shim > declaration, `Expected the shim after the value. Received:\n${css}`, - ); - assert(theme > shim, `Expected the theme after the shim. Received:\n${css}`); + ).toBeTruthy(); + expect( + theme > shim, + `Expected the theme after the shim. Received:\n${css}`, + ).toBeTruthy(); }); -Deno.test("generateJSON - the hex alpha byte rounds from the exact alpha", () => { +test("generateJSON - the hex alpha byte rounds from the exact alpha", () => { // Rounding the alpha to three decimals first moved 12.35% off the byte a // browser paints. const config = defineConfig({ @@ -528,13 +526,13 @@ Deno.test("generateJSON - the hex alpha byte rounds from the exact alpha", () => }, }); - assertEquals(JSON.parse(generateJSON(config)).palette.soft["100"].color, { + expect(JSON.parse(generateJSON(config)).palette.soft["100"].color).toEqual({ hex: { string: "#0000001f" }, rgb: { string: "rgb(0 0 0 / 0.123)" }, }); }); -Deno.test("generateStyleDictionaryJSON - exposes the color outputs beside the resolved value", () => { +test("generateStyleDictionaryJSON - exposes the color outputs beside the resolved value", () => { const config = defineConfig({ colors: { palette: { @@ -550,16 +548,16 @@ Deno.test("generateStyleDictionaryJSON - exposes the color outputs beside the re const token = JSON.parse(generateStyleDictionaryJSON(config)).palette.coral["100"]; - assertEquals(token.$color, { + expect(token.$color).toEqual({ hex: { string: "#ff7f50", number: 16744272 }, rgb: { array: [255, 127, 80] }, }); - assertEquals(token.attributes.color, token.$color); - assertEquals(token.$resolvedValue, "oklch(73.511% 0.16799 40.24666)"); - assertEquals(token.$gamutMapped, undefined); + expect(token.attributes.color).toEqual(token.$color); + expect(token.$resolvedValue).toEqual("oklch(73.511% 0.16799 40.24666)"); + expect(token.$gamutMapped).toEqual(undefined); }); -Deno.test("generateJSON and generateTS - the color outputs are part of the token object", () => { +test("generateJSON and generateTS - the color outputs are part of the token object", () => { const config = defineConfig({ colors: { palette: { @@ -582,14 +580,13 @@ Deno.test("generateJSON and generateTS - the color outputs are part of the token }, }; - assertEquals(generateJSON(config), JSON.stringify(expected, null, 2)); - assertEquals( - generateTS(config), + expect(generateJSON(config)).toEqual(JSON.stringify(expected, null, 2)); + expect(generateTS(config)).toEqual( `export const cssForge = ${JSON.stringify(expected, null, 2)} as const;`, ); }); -Deno.test("generateJSON - token objects omit the color field when none is configured", () => { +test("generateJSON - token objects omit the color field when none is configured", () => { const config = defineConfig({ colors: { palette: { @@ -598,7 +595,7 @@ Deno.test("generateJSON - token objects omit the color field when none is config }, }); - assertEquals(JSON.parse(generateJSON(config)), { + expect(JSON.parse(generateJSON(config))).toEqual({ palette: { coral: { "100": { @@ -611,7 +608,7 @@ Deno.test("generateJSON - token objects omit the color field when none is config }); }); -Deno.test("generateCSS - a whitespace-only selector is read as the root scope", () => { +test("generateCSS - a whitespace-only selector is read as the root scope", () => { const config = defineConfig({ colors: { palette: { @@ -626,8 +623,7 @@ Deno.test("generateCSS - a whitespace-only selector is read as the root scope", }, }); - assertEquals( - generateCSS(config), + expect(generateCSS(config)).toEqual( [ "/*____ CSSForge ____*/", ":root {", @@ -646,7 +642,7 @@ Deno.test("generateCSS - a whitespace-only selector is read as the root scope", ); }); -Deno.test("generateCSS - a shorthand color may keep a variant named color", () => { +test("generateCSS - a shorthand color may keep a variant named color", () => { const config = defineConfig({ colors: { palette: { @@ -655,8 +651,7 @@ Deno.test("generateCSS - a shorthand color may keep a variant named color", () = }, }); - assertEquals( + expect( generateCSS(config).includes("--palette-coral-color: oklch(100% 0 none);"), - true, - ); + ).toEqual(true); }); diff --git a/packages/cssforge/tests/color-mix.test.ts b/packages/cssforge/tests/color-mix.test.ts index dc8f7d0..3c4c4cb 100644 --- a/packages/cssforge/tests/color-mix.test.ts +++ b/packages/cssforge/tests/color-mix.test.ts @@ -1,4 +1,5 @@ import Color from "colorjs.io"; +import { expect, test } from "vitest"; import { generateJSON, generateTS } from "../src/generator.ts"; import type { CSSForgeConfig } from "../src/mod.ts"; import { @@ -7,7 +8,7 @@ import { generateStyleDictionaryJSON, processColors, } from "../src/mod.ts"; -import { assert, assertEquals, assertThrows, Deno } from "./vitest-compat.ts"; +import { thrownError } from "./helpers.ts"; /** The sRGB hex of an oklch color, derived without the mix implementation. */ const hexOf = (l: number, c: number, h: number) => @@ -16,16 +17,16 @@ const hexOf = (l: number, c: number, h: number) => const accent = { base: { hex: "#336699" } }; const throwsAt = (config: unknown, ...fragments: string[]) => { - const error = assertThrows(() => generateCSS(config as CSSForgeConfig)); + const error = thrownError(() => generateCSS(config as CSSForgeConfig)); for (const fragment of fragments) { - assert( + expect( error.message.includes(fragment), `Expected the error to include ${fragment}. Received: ${error.message}`, - ); + ).toBeTruthy(); } }; -Deno.test("generateCSS - a palette mix emits color-mix with a var() reference", () => { +test("generateCSS - a palette mix emits color-mix with a var() reference", () => { const css = generateCSS( defineConfig({ colors: { @@ -41,15 +42,15 @@ Deno.test("generateCSS - a palette mix emits color-mix with a var() reference", }), ); - assert( + expect( css.includes( "--palette-accent-hover: color-mix(in oklch, var(--palette-accent-base), black 15%);", ), css, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - a theme mix derives a variant from a palette color", () => { +test("generateCSS - a theme mix derives a variant from a palette color", () => { const css = generateCSS( defineConfig({ colors: { @@ -71,15 +72,15 @@ Deno.test("generateCSS - a theme mix derives a variant from a palette color", () }), ); - assert( + expect( css.includes( "--theme-light-action-subtle: color-mix(in oklch, var(--palette-accent-base), transparent 88%);", ), css, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - a theme mix can reference an earlier theme token", () => { +test("generateCSS - a theme mix can reference an earlier theme token", () => { const css = generateCSS( defineConfig({ colors: { @@ -103,15 +104,15 @@ Deno.test("generateCSS - a theme mix can reference an earlier theme token", () = }), ); - assert( + expect( css.includes( "--theme-light-action-hover: color-mix(in oklch, var(--theme-light-action-base), white 10%);", ), css, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - literal colors are emitted as written", () => { +test("generateCSS - literal colors are emitted as written", () => { const css = generateCSS( defineConfig({ colors: { @@ -126,15 +127,15 @@ Deno.test("generateCSS - literal colors are emitted as written", () => { }), ); - assert( + expect( css.includes( "--palette-brand-blend: color-mix(in oklch, #ff0000, oklch(50% 0.2 264) 50%);", ), css, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - a fallback format declares the statically mixed color", () => { +test("generateCSS - a fallback format declares the statically mixed color", () => { const css = generateCSS( defineConfig({ colors: { @@ -153,13 +154,13 @@ Deno.test("generateCSS - a fallback format declares the statically mixed color", const [l, c, h] = new Color("#336699").to("oklch").coords; const fallback = css.slice(css.indexOf("@supports")); - assert( + expect( fallback.includes(`--palette-accent-hover: ${hexOf(l * 0.85, c * 0.85, h)};`), css, - ); + ).toBeTruthy(); }); -Deno.test("generateJSON - an achromatic operand keeps the other color's hue", () => { +test("generateJSON - an achromatic operand keeps the other color's hue", () => { const config = defineConfig({ colors: { palette: { @@ -179,10 +180,10 @@ Deno.test("generateJSON - an achromatic operand keeps the other color's hue", () const tint = JSON.parse(generateJSON(config)).palette.base.tint; - assertEquals(tint.color.hex.string, hexOf(0.75, 0.1, 264)); + expect(tint.color.hex.string).toEqual(hexOf(0.75, 0.1, 264)); }); -Deno.test("generateJSON - mixing with transparent keeps the color and sets its alpha", () => { +test("generateJSON - mixing with transparent keeps the color and sets its alpha", () => { const config = defineConfig({ colors: { palette: { @@ -201,13 +202,13 @@ Deno.test("generateJSON - mixing with transparent keeps the color and sets its a const subtle = JSON.parse(generateJSON(config)).palette.accent.subtle; - assertEquals(subtle.color.rgb, { + expect(subtle.color.rgb).toEqual({ string: "rgb(51 102 153 / 0.12)", array: [51, 102, 153, 0.12], }); }); -Deno.test("generateJSON - alpha weights lightness and chroma but not the hue", () => { +test("generateJSON - alpha weights lightness and chroma but not the hue", () => { const config = defineConfig({ colors: { palette: { @@ -230,10 +231,10 @@ Deno.test("generateJSON - alpha weights lightness and chroma but not the hue", ( const blend = JSON.parse(generateJSON(config)).palette.base.blend; - assertEquals(blend.color.hex.string, expected); + expect(blend.color.hex.string).toEqual(expected); }); -Deno.test("generateJSON and generateTS - a mix token keeps its var() reference", () => { +test("generateJSON and generateTS - a mix token keeps its var() reference", () => { const config = defineConfig({ colors: { palette: { @@ -248,15 +249,15 @@ Deno.test("generateJSON and generateTS - a mix token keeps its var() reference", }); const value = "color-mix(in oklch, var(--palette-accent-base), black 15%)"; - assertEquals(JSON.parse(generateJSON(config)).palette.accent.hover, { + expect(JSON.parse(generateJSON(config)).palette.accent.hover).toEqual({ key: "--palette-accent-hover", value, variable: `--palette-accent-hover: ${value};`, }); - assert(generateTS(config).includes(`"value": "${value}"`)); + expect(generateTS(config).includes(`"value": "${value}"`)).toBeTruthy(); }); -Deno.test("processColors - a mix token records the paths it references", () => { +test("processColors - a mix token records the paths it references", () => { const { resolveMap } = processColors({ palette: { value: { @@ -271,15 +272,15 @@ Deno.test("processColors - a mix token records the paths it references", () => { }, }); - assertEquals(resolveMap.get("palette.accent.hover")?.referencePaths, [ + expect(resolveMap.get("palette.accent.hover")?.referencePaths).toEqual([ "palette.accent.base", ]); - assertEquals(resolveMap.get("palette.accent.hover")?.tier, "semantic"); - assertEquals(resolveMap.get("palette.accent.literal")?.referencePaths, undefined); - assertEquals(resolveMap.get("palette.accent.literal")?.tier, "primitive"); + expect(resolveMap.get("palette.accent.hover")?.tier).toEqual("semantic"); + expect(resolveMap.get("palette.accent.literal")?.referencePaths).toEqual(undefined); + expect(resolveMap.get("palette.accent.literal")?.tier).toEqual("primitive"); }); -Deno.test("generateStyleDictionaryJSON - a mix token resolves its reference", () => { +test("generateStyleDictionaryJSON - a mix token resolves its reference", () => { const config = defineConfig({ colors: { palette: { @@ -297,13 +298,13 @@ Deno.test("generateStyleDictionaryJSON - a mix token resolves its reference", () const base = tokens.palette.accent.base.value; const hover = tokens.palette.accent.hover; - assertEquals(hover.value, `color-mix(in oklch, ${base}, black 15%)`); - assertEquals(hover.type, "color"); - assertEquals(hover.$tier, "semantic"); - assertEquals(hover.$reference, "palette.accent.base"); + expect(hover.value).toEqual(`color-mix(in oklch, ${base}, black 15%)`); + expect(hover.type).toEqual("color"); + expect(hover.$tier).toEqual("semantic"); + expect(hover.$reference).toEqual("palette.accent.base"); }); -Deno.test("generateCSS - rejects an unknown key in a mix", () => { +test("generateCSS - rejects an unknown key in a mix", () => { throwsAt( { colors: { @@ -324,7 +325,7 @@ Deno.test("generateCSS - rejects an unknown key in a mix", () => { ); }); -Deno.test("generateCSS - rejects a key beside mix", () => { +test("generateCSS - rejects a key beside mix", () => { throwsAt( { colors: { @@ -346,7 +347,7 @@ Deno.test("generateCSS - rejects a key beside mix", () => { ); }); -Deno.test("generateCSS - rejects an amount outside 0 to 100", () => { +test("generateCSS - rejects an amount outside 0 to 100", () => { for (const amount of [-1, 101, "15", Number.NaN]) { throwsAt( { @@ -366,7 +367,7 @@ Deno.test("generateCSS - rejects an amount outside 0 to 100", () => { } }); -Deno.test("generateCSS - rejects a missing operand", () => { +test("generateCSS - rejects a missing operand", () => { throwsAt( { colors: { @@ -384,7 +385,7 @@ Deno.test("generateCSS - rejects a missing operand", () => { ); }); -Deno.test("generateCSS - rejects an interpolation space other than oklch", () => { +test("generateCSS - rejects an interpolation space other than oklch", () => { throwsAt( { colors: { @@ -410,7 +411,7 @@ Deno.test("generateCSS - rejects an interpolation space other than oklch", () => ); }); -Deno.test("generateCSS - rejects a null interpolation space", () => { +test("generateCSS - rejects a null interpolation space", () => { throwsAt( { colors: { @@ -430,7 +431,7 @@ Deno.test("generateCSS - rejects a null interpolation space", () => { ); }); -Deno.test("generateCSS - a tiny amount is written without exponent notation", () => { +test("generateCSS - a tiny amount is written without exponent notation", () => { const css = generateCSS( defineConfig({ colors: { @@ -448,15 +449,15 @@ Deno.test("generateCSS - a tiny amount is written without exponent notation", () }), ); - assert( + expect( css.includes( "--palette-accent-tiny: color-mix(in oklch, var(--palette-accent-base), black 0.0001%);", ), css, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects a token path that does not resolve", () => { +test("generateCSS - rejects a token path that does not resolve", () => { throwsAt( { colors: { @@ -477,7 +478,7 @@ Deno.test("generateCSS - rejects a token path that does not resolve", () => { ); }); -Deno.test("generateCSS - rejects a token path that is not a color", () => { +test("generateCSS - rejects a token path that is not a color", () => { throwsAt( { colors: { @@ -505,7 +506,7 @@ Deno.test("generateCSS - rejects a token path that is not a color", () => { ); }); -Deno.test("generateCSS - rejects a literal that is not a CSS color", () => { +test("generateCSS - rejects a literal that is not a CSS color", () => { throwsAt( { colors: { @@ -524,7 +525,7 @@ Deno.test("generateCSS - rejects a literal that is not a CSS color", () => { ); }); -Deno.test("generateCSS - rejects an invalid theme mix instead of skipping the theme", () => { +test("generateCSS - rejects an invalid theme mix instead of skipping the theme", () => { throwsAt( { colors: { @@ -544,7 +545,7 @@ Deno.test("generateCSS - rejects an invalid theme mix instead of skipping the th ); }); -Deno.test("generateCSS - rejects a mix with alpha when its format rejects alpha", () => { +test("generateCSS - rejects a mix with alpha when its format rejects alpha", () => { throwsAt( { colors: { diff --git a/packages/cssforge/tests/color-settings-validation.test.ts b/packages/cssforge/tests/color-settings-validation.test.ts index a466e9c..5f93eff 100644 --- a/packages/cssforge/tests/color-settings-validation.test.ts +++ b/packages/cssforge/tests/color-settings-validation.test.ts @@ -1,10 +1,11 @@ +import { expect, test } from "vitest"; import type { CSSForgeConfig } from "../src/mod.ts"; import { defineConfig, generateCSS } from "../src/mod.ts"; -import { assert, assertThrows, Deno } from "./vitest-compat.ts"; +import { thrownError } from "./helpers.ts"; /** Settings are read from a JavaScript object at runtime, so they are validated. */ -Deno.test("generateCSS - rejects a fallback format that is not generated", () => { +test("generateCSS - rejects a fallback format that is not generated", () => { const config = { colors: { palette: { @@ -14,15 +15,15 @@ Deno.test("generateCSS - rejects a fallback format that is not generated", () => }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"rgb"'), `Expected the error to name the missing format. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects a fallback format without its string output", () => { +test("generateCSS - rejects a fallback format without its string output", () => { const config = defineConfig({ colors: { palette: { @@ -34,15 +35,15 @@ Deno.test("generateCSS - rejects a fallback format without its string output", ( }, }); - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"string"'), `Expected the error to ask for the string output. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - a format rejects a color that carries alpha when it sets alpha false", () => { +test("generateCSS - a format rejects a color that carries alpha when it sets alpha false", () => { const config = defineConfig({ colors: { palette: { @@ -55,15 +56,15 @@ Deno.test("generateCSS - a format rejects a color that carries alpha when it set }, }); - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"palette.coral.100"') && error.message.includes('"hex"'), `Expected the error to name the variant and the format. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an unknown color format", () => { +test("generateCSS - rejects an unknown color format", () => { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; const invalidPalette = { colors: { palette: { ...palette, settings: { color: { formats: { hsl: true } } } } }, @@ -81,21 +82,21 @@ Deno.test("generateCSS - rejects an unknown color format", () => { }, } as unknown as CSSForgeConfig; - const paletteError = assertThrows(() => generateCSS(invalidPalette)); - const colorError = assertThrows(() => generateCSS(invalidColor)); + const paletteError = thrownError(() => generateCSS(invalidPalette)); + const colorError = thrownError(() => generateCSS(invalidColor)); - assert( + expect( paletteError.message.includes('"palette.settings.color.formats"') && paletteError.message.includes("hsl"), `Expected the error to name the path and the value. Received: ${paletteError.message}`, - ); - assert( + ).toBeTruthy(); + expect( colorError.message.includes('"palette.coral.settings.color.formats"'), `Expected the error to name "palette.coral.settings.color.formats". Received: ${colorError.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an output a format does not produce", () => { +test("generateCSS - rejects an output a format does not produce", () => { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; const arrayOnHex = { colors: { @@ -114,27 +115,27 @@ Deno.test("generateCSS - rejects an output a format does not produce", () => { colors: { palette: { ...palette, settings: { color: { formats: { hex: {} } } } } }, } as unknown as CSSForgeConfig; - const hexError = assertThrows(() => generateCSS(arrayOnHex)); - const rgbError = assertThrows(() => generateCSS(digitsOnRgb)); - const emptyError = assertThrows(() => generateCSS(nothingEnabled)); + const hexError = thrownError(() => generateCSS(arrayOnHex)); + const rgbError = thrownError(() => generateCSS(digitsOnRgb)); + const emptyError = thrownError(() => generateCSS(nothingEnabled)); - assert( + expect( hexError.message.includes('"palette.settings.color.formats.hex"') && hexError.message.includes('"digits"'), `Expected the error to list the hex outputs. Received: ${hexError.message}`, - ); - assert( + ).toBeTruthy(); + expect( rgbError.message.includes('"palette.settings.color.formats.rgb"') && rgbError.message.includes('"array"'), `Expected the error to list the rgb outputs. Received: ${rgbError.message}`, - ); - assert( + ).toBeTruthy(); + expect( emptyError.message.includes('"palette.settings.color.formats.hex"'), `Expected the error to name the empty format. Received: ${emptyError.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an alpha that is not a boolean or an opacity", () => { +test("generateCSS - rejects an alpha that is not a boolean or an opacity", () => { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; const notAnOpacity = { colors: { @@ -145,15 +146,15 @@ Deno.test("generateCSS - rejects an alpha that is not a boolean or an opacity", }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(notAnOpacity)); + const error = thrownError(() => generateCSS(notAnOpacity)); - assert( + expect( error.message.includes('"palette.settings.color.formats.hex.alpha"'), `Expected the error to name the format's alpha. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an alpha outside a format", () => { +test("generateCSS - rejects an alpha outside a format", () => { const config = { colors: { palette: { @@ -163,16 +164,16 @@ Deno.test("generateCSS - rejects an alpha outside a format", () => { }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"palette.settings.color"') && error.message.includes("formats.hex.alpha"), `Expected the error to point at the format. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects color settings that are not inside settings", () => { +test("generateCSS - rejects color settings that are not inside settings", () => { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; const colorOnPalette = { colors: { palette: { ...palette, color: { formats: { hex: true } } } }, @@ -193,43 +194,43 @@ Deno.test("generateCSS - rejects color settings that are not inside settings", ( }, } as unknown as CSSForgeConfig; - const paletteError = assertThrows(() => generateCSS(colorOnPalette)); - const settingsError = assertThrows(() => generateCSS(formatsInSettings)); - const colorError = assertThrows(() => generateCSS(fallbackOnColor)); + const paletteError = thrownError(() => generateCSS(colorOnPalette)); + const settingsError = thrownError(() => generateCSS(formatsInSettings)); + const colorError = thrownError(() => generateCSS(fallbackOnColor)); - assert( + expect( paletteError.message.includes('"palette"'), `Expected the error to name "palette". Received: ${paletteError.message}`, - ); - assert( + ).toBeTruthy(); + expect( settingsError.message.includes('"palette.settings.color"'), `Expected the error to name "palette.settings.color". Received: ${settingsError.message}`, - ); - assert( + ).toBeTruthy(); + expect( colorError.message.includes('"palette.coral"'), `Expected the error to name "palette.coral". Received: ${colorError.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an unknown key in settings", () => { +test("generateCSS - rejects an unknown key in settings", () => { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; const misspelled = { colors: { palette: { ...palette, settings: { colour: { formats: { hex: true } } } } }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(misspelled)); + const error = thrownError(() => generateCSS(misspelled)); - assert( + expect( error.message.includes('"palette.settings"') && error.message.includes('"colour"'), `Expected the error to name the path and the unknown key. Received: ${error.message}`, - ); - assert( + ).toBeTruthy(); + expect( error.message.includes('"color"'), `Expected the error to list the accepted key. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an unknown key beside the formats", () => { +test("generateCSS - rejects an unknown key beside the formats", () => { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; const misspelled = { colors: { @@ -240,17 +241,17 @@ Deno.test("generateCSS - rejects an unknown key beside the formats", () => { }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(misspelled)); + const error = thrownError(() => generateCSS(misspelled)); - assert( + expect( error.message.includes('"palette.settings.color"') && error.message.includes('"alfpha"') && error.message.includes('"fallback"'), `Expected the error to name the path, the unknown key and the accepted keys. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an unknown key on a palette color entry", () => { +test("generateCSS - rejects an unknown key on a palette color entry", () => { const unknownKey = { colors: { palette: { @@ -277,22 +278,22 @@ Deno.test("generateCSS - rejects an unknown key on a palette color entry", () => }, } as unknown as CSSForgeConfig; - const entryError = assertThrows(() => generateCSS(unknownKey)); - const settingsError = assertThrows(() => generateCSS(unknownSetting)); + const entryError = thrownError(() => generateCSS(unknownKey)); + const settingsError = thrownError(() => generateCSS(unknownSetting)); - assert( + expect( entryError.message.includes('"palette.coral"') && entryError.message.includes('"selectr"'), `Expected the error to name the entry and the key. Received: ${entryError.message}`, - ); - assert( + ).toBeTruthy(); + expect( settingsError.message.includes('"palette.coral.settings"') && settingsError.message.includes('"selector"'), `Expected the error to name the settings and the accepted keys. Received: ${settingsError.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects color format settings on gradients and themes", () => { +test("generateCSS - rejects color format settings on gradients and themes", () => { const base = { palette: { value: { coral: { 100: { hex: "#FF7F50" } } } } }; const onGradient = { colors: { @@ -320,22 +321,22 @@ Deno.test("generateCSS - rejects color format settings on gradients and themes", }, } as unknown as CSSForgeConfig; - const gradientError = assertThrows(() => generateCSS(onGradient)); - const themeError = assertThrows(() => generateCSS(onTheme)); + const gradientError = thrownError(() => generateCSS(onGradient)); + const themeError = thrownError(() => generateCSS(onTheme)); - assert( + expect( gradientError.message.includes('"gradients.settings"') && gradientError.message.includes("palette"), `Expected the error to point at the palette. Received: ${gradientError.message}`, - ); - assert( + ).toBeTruthy(); + expect( themeError.message.includes('"theme.light.settings"') && themeError.message.includes("palette"), `Expected the error to point at the palette. Received: ${themeError.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - a fallback error names the color it was resolved for", () => { +test("generateCSS - a fallback error names the color it was resolved for", () => { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; const namedButMissing = { colors: { @@ -356,22 +357,22 @@ Deno.test("generateCSS - a fallback error names the color it was resolved for", }, } as unknown as CSSForgeConfig; - const missingError = assertThrows(() => generateCSS(namedButMissing)); - const stringError = assertThrows(() => generateCSS(namedWithoutString)); + const missingError = thrownError(() => generateCSS(namedButMissing)); + const stringError = thrownError(() => generateCSS(namedWithoutString)); - assert( + expect( missingError.message.includes('"palette.coral"') && missingError.message.includes('"rgb"'), `Expected the error to name the color and the format. Received: ${missingError.message}`, - ); - assert( + ).toBeTruthy(); + expect( stringError.message.includes('"palette.coral"') && stringError.message.includes('"string"'), `Expected the error to name the color and the output. Received: ${stringError.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects color format settings on a theme color", () => { +test("generateCSS - rejects color format settings on a theme color", () => { // This check used to sit inside the theme try block, which logged and dropped // the theme instead of reporting the configuration. const config = { @@ -391,15 +392,15 @@ Deno.test("generateCSS - rejects color format settings on a theme color", () => }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"theme.light.content.settings"'), `Expected the error to name the theme color. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects unknown keys at the gradient and theme levels", () => { +test("generateCSS - rejects unknown keys at the gradient and theme levels", () => { const base = { palette: { value: { coral: { 100: { hex: "#FF7F50" } } } } }; const onTheme = { colors: { @@ -443,42 +444,42 @@ Deno.test("generateCSS - rejects unknown keys at the gradient and theme levels", }, } as unknown as CSSForgeConfig; - const themeError = assertThrows(() => generateCSS(onTheme)); - const themeColorError = assertThrows(() => generateCSS(onThemeColor)); - const gradientError = assertThrows(() => generateCSS(onGradientLevel)); + const themeError = thrownError(() => generateCSS(onTheme)); + const themeColorError = thrownError(() => generateCSS(onThemeColor)); + const gradientError = thrownError(() => generateCSS(onGradientLevel)); - assert( + expect( themeError.message.includes('"theme.light.settings"') && themeError.message.includes('"selectr"'), `Expected the theme settings key to be rejected. Received: ${themeError.message}`, - ); - assert( + ).toBeTruthy(); + expect( themeColorError.message.includes('"theme.light.content.settings"') && themeColorError.message.includes('"variantNameOnly"'), `Expected the theme color settings key to be rejected. Received: ${themeColorError.message}`, - ); - assert( + ).toBeTruthy(); + expect( gradientError.message.includes('"gradients.settings"') && gradientError.message.includes("reads no settings"), `Expected the unread gradient settings to be rejected. Received: ${gradientError.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects formats set to false", () => { +test("generateCSS - rejects formats set to false", () => { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; const config = { colors: { palette: { ...palette, settings: { color: { formats: false } } } }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"palette.settings.color.formats"'), `Expected the error to name the formats. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects settings a gradient variant does not read", () => { +test("generateCSS - rejects settings a gradient variant does not read", () => { const config = { colors: { palette: { value: { coral: { 100: { hex: "#FF7F50" } } } }, @@ -497,11 +498,11 @@ Deno.test("generateCSS - rejects settings a gradient variant does not read", () }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"gradients.g.primary.settings"') && error.message.includes("reads no settings"), `Expected the error to name the variant. Received: ${error.message}`, - ); + ).toBeTruthy(); }); diff --git a/packages/cssforge/tests/colors.test.ts b/packages/cssforge/tests/colors.test.ts index 188e89b..2ca7afb 100644 --- a/packages/cssforge/tests/colors.test.ts +++ b/packages/cssforge/tests/colors.test.ts @@ -1,8 +1,8 @@ +import { expect, test } from "vitest"; import { defineConfig, processColors } from "../src/mod.ts"; import { combine, getLines } from "./helpers.ts"; -import { assert, assertEquals, assertSnapshot, Deno } from "./vitest-compat.ts"; -Deno.test("processColors - converts hex to oklch", async (t) => { +test("processColors - converts hex to oklch", () => { const config = defineConfig({ colors: { palette: { @@ -21,14 +21,21 @@ Deno.test("processColors - converts hex to oklch", async (t) => { const { css, resolveMap } = processColors(config.colors); const combined = combine(css); const lines = getLines(combined); - assertEquals(lines[2].trim(), "--palette-coral-100: oklch(73.511% 0.16799 40.24666);"); - assertEquals(lines[3].trim(), "--palette-coral-200: oklch(69.622% 0.19552 32.32143);"); - assertEquals(Array.from(resolveMap.keys()), ["palette.coral.100", "palette.coral.200"]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(lines[2].trim()).toEqual( + "--palette-coral-100: oklch(73.511% 0.16799 40.24666);", + ); + expect(lines[3].trim()).toEqual( + "--palette-coral-200: oklch(69.622% 0.19552 32.32143);", + ); + expect(Array.from(resolveMap.keys())).toEqual([ + "palette.coral.100", + "palette.coral.200", + ]); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles different color formats", async (t) => { +test("processColors - handles different color formats", () => { const config = defineConfig({ colors: { palette: { @@ -49,25 +56,28 @@ Deno.test("processColors - handles different color formats", async (t) => { const combined = combine(css); const lines = getLines(combined); - assertEquals(lines[2].trim(), "--palette-brand-200: oklch(86.644% 0.29483 142.49535);"); - assertEquals(lines[3].trim(), "--palette-brand-300: oklch(45.201% 0.31321 264.05202);"); - assertEquals(lines[4].trim(), "--palette-brand-400: oklch(70% 0.2 270);"); - assertEquals( - lines[5].trim(), + expect(lines[2].trim()).toEqual( + "--palette-brand-200: oklch(86.644% 0.29483 142.49535);", + ); + expect(lines[3].trim()).toEqual( + "--palette-brand-300: oklch(45.201% 0.31321 264.05202);", + ); + expect(lines[4].trim()).toEqual("--palette-brand-400: oklch(70% 0.2 270);"); + expect(lines[5].trim()).toEqual( "--palette-brand-default: oklch(62.796% 0.25768 29.23388);", ); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "palette.brand.200", "palette.brand.300", "palette.brand.400", "palette.brand.default", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles string values", async (t) => { +test("processColors - handles string values", () => { const config = defineConfig({ colors: { palette: { @@ -85,17 +95,17 @@ Deno.test("processColors - handles string values", async (t) => { const { css, resolveMap } = processColors(config.colors); const combined = combine(css); const lines = getLines(combined); - assertEquals(lines[2].trim(), "--palette-simple-white: oklch(100% 0 none);"); - assertEquals(lines[3].trim(), "--palette-simple-black: oklch(0% 0 none);"); - assertEquals(Array.from(resolveMap.keys()), [ + expect(lines[2].trim()).toEqual("--palette-simple-white: oklch(100% 0 none);"); + expect(lines[3].trim()).toEqual("--palette-simple-black: oklch(0% 0 none);"); + expect(Array.from(resolveMap.keys())).toEqual([ "palette.simple.white", "palette.simple.black", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles direct palette groups", async (t) => { +test("processColors - handles direct palette groups", () => { const config = defineConfig({ colors: { palette: { @@ -129,23 +139,22 @@ Deno.test("processColors - handles direct palette groups", async (t) => { const { css, resolveMap } = processColors(config.colors); const combined = combine(css); - assertEquals(combined.includes("--palette-basic-white: oklch(100% 0 none);"), true); - assertEquals(combined.includes("--palette-gray-950: oklch(14.479% 0 none);"), true); - assertEquals( + expect(combined.includes("--palette-basic-white: oklch(100% 0 none);")).toEqual(true); + expect(combined.includes("--palette-gray-950: oklch(14.479% 0 none);")).toEqual(true); + expect( combined.includes("--theme-light-content-primary: var(--palette-gray-950);"), - true, - ); - assertEquals(Array.from(resolveMap.keys()), [ + ).toEqual(true); + expect(Array.from(resolveMap.keys())).toEqual([ "palette.basic.white", "palette.basic.black", "palette.gray.950", "theme.light.content.primary", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles themes", async (t) => { +test("processColors - handles themes", () => { const config = defineConfig({ colors: { palette: { @@ -179,25 +188,23 @@ Deno.test("processColors - handles themes", async (t) => { const { css, resolveMap } = processColors(config.colors); const combined = combine(css); const lines = getLines(combined); - assertEquals( - lines[7].trim(), + expect(lines[7].trim()).toEqual( "--theme-light-background-primary: var(--palette-simple-white);", ); - assertEquals( - lines[8].trim(), + expect(lines[8].trim()).toEqual( "--theme-light-background-secondary: var(--palette-simple-black);", ); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "palette.simple.white", "palette.simple.black", "theme.light.background.primary", "theme.light.background.secondary", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles transparency", async (t) => { +test("processColors - handles transparency", () => { const config = defineConfig({ colors: { palette: { @@ -215,17 +222,21 @@ Deno.test("processColors - handles transparency", async (t) => { const { css, resolveMap } = processColors(config.colors); const combined = combine(css); const lines = getLines(combined); - assertEquals(lines[2].trim(), "--palette-alpha-softGray1: oklch(14.48% 0 none / 12%);"); - assertEquals(lines[3].trim(), "--palette-alpha-softGray2: oklch(14.48% 0 none / 24%);"); - assertEquals(Array.from(resolveMap.keys()), [ + expect(lines[2].trim()).toEqual( + "--palette-alpha-softGray1: oklch(14.48% 0 none / 12%);", + ); + expect(lines[3].trim()).toEqual( + "--palette-alpha-softGray2: oklch(14.48% 0 none / 24%);", + ); + expect(Array.from(resolveMap.keys())).toEqual([ "palette.alpha.softGray1", "palette.alpha.softGray2", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - generates gradient with color variables", async (t) => { +test("processColors - generates gradient with color variables", () => { const config = defineConfig({ colors: { palette: { @@ -272,19 +283,19 @@ Deno.test("processColors - generates gradient with color variables", async (t) = "--gradients-orangeGradient-primary: linear-gradient(261.78deg, var(--palette-coral-50) 33.1%, var(--palette-coral-90) 56.3%, var(--palette-coral-100) 65.78%, var(--palette-indigo-100) 84.23%);", ].join("\n"); - assertEquals(lines.at(-1), expected); - assertEquals(Array.from(result.resolveMap.keys()), [ + expect(lines.at(-1)).toEqual(expected); + expect(Array.from(result.resolveMap.keys())).toEqual([ "palette.coral.50", "palette.coral.90", "palette.coral.100", "palette.indigo.100", "gradients.orangeGradient.primary", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles themes referencing gradients", async (t) => { +test("processColors - handles themes referencing gradients", () => { const config = defineConfig({ colors: { palette: { @@ -332,20 +343,19 @@ Deno.test("processColors - handles themes referencing gradients", async (t) => { const combined = combine(css); const lines = getLines(combined); const lastLine = lines.pop(); - assertEquals( - lastLine, + expect(lastLine).toEqual( "--theme-light-background-primary: var(--gradients-orangeGradient-primary);", ); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "palette.coral.50", "gradients.orangeGradient.primary", "theme.light.background.primary", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles gradients with atRule", async (t) => { +test("processColors - handles gradients with atRule", () => { const config = defineConfig({ colors: { palette: { @@ -386,32 +396,31 @@ Deno.test("processColors - handles gradients with atRule", async (t) => { line.includes("@media (prefers-color-scheme: dark)"), ); - assertEquals(lines[mediaQueryIndex], "@media (prefers-color-scheme: dark) {"); + expect(lines[mediaQueryIndex]).toEqual("@media (prefers-color-scheme: dark) {"); const varIndex = lines.findIndex( (line, idx) => idx > mediaQueryIndex && line.includes("--gradients-orangeGradient-primary:"), ); - assert( + expect( varIndex > mediaQueryIndex, "Gradient variable should be inside the media query block", - ); + ).toBeTruthy(); - assertEquals( - lines[varIndex].trim(), + expect(lines[varIndex].trim()).toEqual( "--gradients-orangeGradient-primary: linear-gradient(to right, var(--palette-coral-50), var(--palette-coral-50));", ); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "palette.coral.50", "gradients.orangeGradient.primary", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles palette colors with atRule", async (t) => { +test("processColors - handles palette colors with atRule", () => { const config = defineConfig({ colors: { palette: { @@ -437,37 +446,39 @@ Deno.test("processColors - handles palette colors with atRule", async (t) => { const combined = combine(css); const lines = getLines(combined); - assertEquals(lines[2].trim(), "--palette-background-light: oklch(100% 0 none);"); + expect(lines[2].trim()).toEqual("--palette-background-light: oklch(100% 0 none);"); const mediaQueryIndex = lines.findIndex((line) => line.includes("@media (prefers-color-scheme: dark)"), ); - assertEquals(lines[mediaQueryIndex], "@media (prefers-color-scheme: dark) {"); + expect(lines[mediaQueryIndex]).toEqual("@media (prefers-color-scheme: dark) {"); const darkVarIndex = lines.findIndex((line) => line.includes("--palette-backgroundDark-dark:"), ); - assertEquals( - lines[darkVarIndex].trim(), + expect(lines[darkVarIndex].trim()).toEqual( "--palette-backgroundDark-dark: oklch(0% 0 none);", ); const closingBraceIndex = lines.findIndex( (line, idx) => idx > mediaQueryIndex && line === "}", ); - assert(closingBraceIndex > darkVarIndex, "Closing brace should exist after variables"); + expect( + closingBraceIndex > darkVarIndex, + "Closing brace should exist after variables", + ).toBeTruthy(); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "palette.background.light", "palette.backgroundDark.dark", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles themes with selector", async (t) => { +test("processColors - handles themes with selector", () => { const config = defineConfig({ colors: { palette: { @@ -504,20 +515,23 @@ Deno.test("processColors - handles themes with selector", async (t) => { const lines = getLines(combined); const classIndex = lines.findIndex((l) => l.includes(".dark-theme")); - assertEquals(lines[classIndex], ".dark-theme {"); + expect(lines[classIndex]).toEqual(".dark-theme {"); const varIndex = lines.findIndex((l) => l.includes("--theme-dark-background-primary:")); - assert(varIndex > classIndex, "theme variable should be inside the class block"); + expect( + varIndex > classIndex, + "theme variable should be inside the class block", + ).toBeTruthy(); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "palette.base.primary", "theme.dark.background.primary", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles selector + atRule", async (t) => { +test("processColors - handles selector + atRule", () => { const config = defineConfig({ colors: { palette: { @@ -557,32 +571,35 @@ Deno.test("processColors - handles selector + atRule", async (t) => { const mediaIndex = lines.findIndex((l) => l.includes("@media (prefers-color-scheme: dark)"), ); - assertEquals(lines[mediaIndex], "@media (prefers-color-scheme: dark) {"); + expect(lines[mediaIndex]).toEqual("@media (prefers-color-scheme: dark) {"); const classIndex = lines.findIndex( (l, idx) => idx > mediaIndex && l.includes(".dark-theme"), ); - assert(classIndex > mediaIndex, "selector should be inside the media query block"); - assertEquals(lines[classIndex].trim(), ".dark-theme {"); + expect( + classIndex > mediaIndex, + "selector should be inside the media query block", + ).toBeTruthy(); + expect(lines[classIndex].trim()).toEqual(".dark-theme {"); const varIndex = lines.findIndex( (l, idx) => idx > classIndex && l.includes("--theme-dark-background-primary:"), ); - assert( + expect( varIndex > classIndex, "theme variable should be inside the selector block within the media query", - ); + ).toBeTruthy(); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "palette.base.primary", "theme.dark.background.primary", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - handles theme variantNameOnly", async (t) => { +test("processColors - handles theme variantNameOnly", () => { const config = defineConfig({ colors: { palette: { @@ -624,21 +641,21 @@ Deno.test("processColors - handles theme variantNameOnly", async (t) => { const primaryLine = lines.find((l) => l.includes("--primary:")); const secondaryLine = lines.find((l) => l.includes("--secondary:")); - assertEquals(primaryLine?.trim(), "--primary: var(--palette-simple-white);"); - assertEquals(secondaryLine?.trim(), "--secondary: var(--palette-simple-black);"); + expect(primaryLine?.trim()).toEqual("--primary: var(--palette-simple-white);"); + expect(secondaryLine?.trim()).toEqual("--secondary: var(--palette-simple-black);"); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "palette.simple.white", "palette.simple.black", "theme.light.background.primary", "theme.light.background.secondary", ]); - await assertSnapshot(t, combined); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(combined).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processColors - achromatic colors emit hue as none", async (t) => { +test("processColors - achromatic colors emit hue as none", () => { const config = defineConfig({ colors: { palette: { @@ -667,34 +684,31 @@ Deno.test("processColors - achromatic colors emit hue as none", async (t) => { const combined = combine(css); const lines = getLines(combined); - assertEquals( - lines.find((l) => l.includes("--palette-achromatic-white:"))?.trim(), + expect(lines.find((l) => l.includes("--palette-achromatic-white:"))?.trim()).toEqual( "--palette-achromatic-white: oklch(100% 0 none);", ); - assertEquals( - lines.find((l) => l.includes("--palette-achromatic-black:"))?.trim(), + expect(lines.find((l) => l.includes("--palette-achromatic-black:"))?.trim()).toEqual( "--palette-achromatic-black: oklch(0% 0 none);", ); - assertEquals( - lines.find((l) => l.includes("--palette-achromatic-gray:"))?.trim(), + expect(lines.find((l) => l.includes("--palette-achromatic-gray:"))?.trim()).toEqual( "--palette-achromatic-gray: oklch(59.987% 0 none);", ); - assertEquals( + expect( lines.find((l) => l.includes("--palette-achromatic-oklchZeroChroma:"))?.trim(), - "--palette-achromatic-oklchZeroChroma: oklch(50% 0 none);", - ); - assertEquals( + ).toEqual("--palette-achromatic-oklchZeroChroma: oklch(50% 0 none);"); + expect( lines.find((l) => l.includes("--palette-achromatic-oklchRedHue:"))?.trim(), - "--palette-achromatic-oklchRedHue: oklch(50% 0 none);", - ); - assertEquals( + ).toEqual("--palette-achromatic-oklchRedHue: oklch(50% 0 none);"); + expect( lines.find((l) => l.includes("--palette-achromatic-oklchTuple:"))?.trim(), - "--palette-achromatic-oklchTuple: oklch(50% 0 none);", - ); + ).toEqual("--palette-achromatic-oklchTuple: oklch(50% 0 none);"); const redLine = lines.find((l) => l.includes("--palette-chromatic-red:"))?.trim(); - assert(redLine?.includes("oklch(")); - assert(!redLine?.includes("none"), "chromatic colors should have a numeric hue"); + expect(redLine?.includes("oklch(")).toBeTruthy(); + expect( + !redLine?.includes("none"), + "chromatic colors should have a numeric hue", + ).toBeTruthy(); - await assertSnapshot(t, combined); + expect(combined).toMatchSnapshot(); }); diff --git a/packages/cssforge/tests/fluid-pow-scale.test.ts b/packages/cssforge/tests/fluid-pow-scale.test.ts index bd2b2bd..f63e8fa 100644 --- a/packages/cssforge/tests/fluid-pow-scale.test.ts +++ b/packages/cssforge/tests/fluid-pow-scale.test.ts @@ -1,4 +1,5 @@ import { calculateTypeScale } from "utopia-core"; +import { expect, test } from "vitest"; import { generateJSON, generateTS } from "../src/generator.ts"; import type { CSSForgeConfig } from "../src/mod.ts"; import { @@ -7,8 +8,7 @@ import { generateStyleDictionaryJSON, getDiagnostics, } from "../src/mod.ts"; -import { getLines } from "./helpers.ts"; -import { assert, assertEquals, assertThrows, Deno } from "./vitest-compat.ts"; +import { getLines, thrownError } from "./helpers.ts"; type FluidScale = NonNullable[string]; type FluidSettings = NonNullable; @@ -47,10 +47,10 @@ const declarations = (css: string) => const steps = [4, 3, 2, 1, 0, -1, -2] as const; const labels = { 4: "3xl", 3: "2xl", 2: "xl", 1: "l", 0: "m", [-1]: "s", [-2]: "xs" }; -Deno.test("fluid type - every scale is written as clamp() steps and as pow tokens", () => { +test("fluid type - every scale is written as clamp() steps and as pow tokens", () => { const utopia = calculateTypeScale({ ...example, labelStyle: "tshirt" }); - assertEquals(getLines(generateCSS(scale(example))).slice(3, -1), [ + expect(getLines(generateCSS(scale(example))).slice(3, -1)).toEqual([ ...utopia .slice(0, 5) .map(({ label, clamp }) => `--typography_fluid-body-${label}: ${clamp};`), @@ -76,7 +76,7 @@ Deno.test("fluid type - every scale is written as clamp() steps and as pow token ]); }); -Deno.test("fluid type - clamp() steps 0 and above are utopia's, byte for byte", () => { +test("fluid type - clamp() steps 0 and above are utopia's, byte for byte", () => { for (const value of [ example, { ...example, relativeTo: "container" as const }, @@ -95,7 +95,7 @@ Deno.test("fluid type - clamp() steps 0 and above are utopia's, byte for byte", ...value, labelStyle: "tshirt", })) { - if (step >= 0) assertEquals(css.get(`--typography_fluid-body-${label}`), clamp); + if (step >= 0) expect(css.get(`--typography_fluid-body-${label}`)).toEqual(clamp); } } }); @@ -109,7 +109,7 @@ const evaluate = (css: Map, name: string, width: number): number const substitute = (value: string): string => value.replace(/var\((--[\w-]+)\)/g, (_, inner: string) => { const declared = css.get(inner); - assert(declared !== undefined, `Undeclared ${inner}`); + expect(declared !== undefined, `Undeclared ${inner}`).toBeTruthy(); return `(${substitute(declared ?? "")})`; }); const expression = substitute(css.get(name) ?? "") @@ -118,13 +118,13 @@ const evaluate = (css: Map, name: string, width: number): number .replaceAll("calc(", "(") .replaceAll("clamp(", "clampPx(") .replaceAll("pow(", "Math.pow("); - assert(/^[\d\s.+\-*/(),a-zA-Z]+$/.test(expression), expression); + expect(/^[\d\s.+\-*/(),a-zA-Z]+$/.test(expression), expression).toBeTruthy(); const clampPx = (min: number, preferred: number, max: number) => Math.min(max, Math.max(min, preferred)); return new Function("clampPx", `return ${expression};`)(clampPx) as number; }; -Deno.test("fluid type - both representations give every step its size at both widths", () => { +test("fluid type - both representations give every step its size at both widths", () => { const value = { ...example, minFontSize: 16, maxFontSize: 21, maxTypeScale: 1.333 }; const css = declarations(generateCSS(scale(value))); @@ -149,13 +149,19 @@ Deno.test("fluid type - both representations give every step its size at both wi ] as const) { const pow = evaluate(css, `--typography_fluid-body-pow-${label}`, width); const clamp = evaluate(css, `--typography_fluid-body-${label}`, width); - assert(Math.abs(pow - expected) < 1e-9, `pow ${label} at ${width}px: ${pow}`); - assert(Math.abs(clamp - expected) < 0.02, `clamp ${label} at ${width}px: ${clamp}`); + expect( + Math.abs(pow - expected) < 1e-9, + `pow ${label} at ${width}px: ${pow}`, + ).toBeTruthy(); + expect( + Math.abs(clamp - expected) < 0.02, + `clamp ${label} at ${width}px: ${clamp}`, + ).toBeTruthy(); } } }); -Deno.test("fluid type - JSON, TypeScript and Style Dictionary hold both representations as the CSS writes them", () => { +test("fluid type - JSON, TypeScript and Style Dictionary hold both representations as the CSS writes them", () => { const config = scale(example); const css = declarations(generateCSS(config)); type Token = { key: string; value: string; variable: string }; @@ -172,20 +178,10 @@ Deno.test("fluid type - JSON, TypeScript and Style Dictionary hold both represen ...Object.values(powSteps), ]; - assertEquals( - jsonTokens.map(({ key, value }) => [key, value]), - [...css.entries()], - ); - assertEquals( - Object.keys(clampSteps), - steps.map((step) => `body@${labels[step]}`), - ); - assertEquals( - Object.keys(powSteps), - steps.map((step) => `pow@${labels[step]}`), - ); - assertEquals( - generateTS(config), + expect(jsonTokens.map(({ key, value }) => [key, value])).toEqual([...css.entries()]); + expect(Object.keys(clampSteps)).toEqual(steps.map((step) => `body@${labels[step]}`)); + expect(Object.keys(powSteps)).toEqual(steps.map((step) => `pow@${labels[step]}`)); + expect(generateTS(config)).toEqual( `export const cssForge = ${generateJSON(config)} as const;`, ); @@ -204,28 +200,27 @@ Deno.test("fluid type - JSON, TypeScript and Style Dictionary hold both represen }; const { pow: sdPow, ...sdClamp } = styleDictionary["typography-fluid"].body; const sdTokens = [...Object.values(sdClamp), ...Object.values(sdPow)]; - assertEquals( - sdTokens.map(({ attributes }) => attributes.cssVariable).sort(), + expect(sdTokens.map(({ attributes }) => attributes.cssVariable).sort()).toEqual( [...css.keys()].sort(), ); // Plain values are primitives; a value made of other tokens is semantic and // names them, as a color mix does. - assertEquals(sdPow["min-width"]?.$tier, "primitive"); - assertEquals(sdClamp.m?.$tier, "primitive"); - assertEquals(sdPow.progress?.attributes.referencePaths, [ + expect(sdPow["min-width"]?.$tier).toEqual("primitive"); + expect(sdClamp.m?.$tier).toEqual("primitive"); + expect(sdPow.progress?.attributes.referencePaths).toEqual([ "typography-fluid.body.pow.min-width", "typography-fluid.body.pow.max-width", ]); - assertEquals(sdPow.s?.$tier, "semantic"); - assertEquals(sdPow.s?.attributes.referencePaths, [ + expect(sdPow.s?.$tier).toEqual("semantic"); + expect(sdPow.s?.attributes.referencePaths).toEqual([ "typography-fluid.body.pow.m", "typography-fluid.body.pow.min-type-scale", ]); - assertEquals(sdPow.s?.$reference, "typography-fluid.body.pow.m"); + expect(sdPow.s?.$reference).toEqual("typography-fluid.body.pow.m"); }); -Deno.test("fluid type - a primitive references pow tokens like any other token", () => { +test("fluid type - a primitive references pow tokens like any other token", () => { const value = { ...example, positiveSteps: 1, negativeSteps: 1, prefix: "text" }; const customLabel = { "-1": "small", "0": "base", "1": "large" }; const config = defineConfig({ @@ -251,61 +246,53 @@ Deno.test("fluid type - a primitive references pow tokens like any other token", }); const css = declarations(generateCSS(config)); - assertEquals( - css.get("--heading-h1-fontSize"), + expect(css.get("--heading-h1-fontSize")).toEqual( "var(--typography_fluid-body-text-pow-large)", ); - assertEquals( - css.get("--heading-h1-lineHeight"), + expect(css.get("--heading-h1-lineHeight")).toEqual( "calc(var(--typography_fluid-body-text-pow-max-type-scale) * 1.1)", ); - assertEquals( - css.get("--heading-h1-padding"), + expect(css.get("--heading-h1-padding")).toEqual( "var(--typography_fluid-body-text-small)", ); - assertEquals( - css.get("--typography_fluid-body-text-pow-small"), + expect(css.get("--typography_fluid-body-text-pow-small")).toEqual( "calc(var(--typography_fluid-body-text-pow-base) / var(--typography_fluid-body-text-pow-min-type-scale))", ); const json = JSON.parse(generateJSON(config)) as { primitives: { heading: { h1: { fontSize: { value: string } } } }; }; - assertEquals( - json.primitives.heading.h1.fontSize.value, + expect(json.primitives.heading.h1.fontSize.value).toEqual( "var(--typography_fluid-body-text-pow-large)", ); }); -Deno.test("fluid type - a step label that takes a pow name fails as a collision", () => { +test("fluid type - a step label that takes a pow name fails as a collision", () => { const collision = (customLabel: Record) => - assertThrows(() => generateJSON(scale(example, { customLabel }))).message; + thrownError(() => generateJSON(scale(example, { customLabel }))).message; - assertEquals( - collision({ "0": "min-width" }), + expect(collision({ "0": "min-width" })).toEqual( 'Token key collision: "typography_fluid.body.pow.min-width" and "typography_fluid.body.pow@min-width" both generate "--typography_fluid-body-pow-min-width". Rename one of the configuration paths.', ); - assertEquals( - collision({ "1": "pow-min-width" }), + expect(collision({ "1": "pow-min-width" })).toEqual( 'Token key collision: "typography_fluid.body@pow-min-width" and "typography_fluid.body.pow.min-width" both generate "--typography_fluid-body-pow-min-width". Rename one of the configuration paths.', ); - assertEquals( - collision({ "0": "pow-l", "1": "l" }), + expect(collision({ "0": "pow-l", "1": "l" })).toEqual( 'Token key collision: "typography_fluid.body@pow-l" and "typography_fluid.body.pow@l" both generate "--typography_fluid-body-pow-l". Rename one of the configuration paths.', ); - assertThrows(() => generateCSS(scale(example, { customLabel: { "0": "min-width" } }))); + thrownError(() => generateCSS(scale(example, { customLabel: { "0": "min-width" } }))); // `pow` is the segment that holds the pow tokens, so no step takes it. - const segment = assertThrows(() => + const segment = thrownError(() => generateCSS(scale(example, { customLabel: { "0": "pow" } })), ); - assert( + expect( segment.message.includes('"typography_fluid.body.settings.customLabel.0"'), segment.message, - ); - assert(segment.message.includes('"pow"'), segment.message); + ).toBeTruthy(); + expect(segment.message.includes('"pow"'), segment.message).toBeTruthy(); }); -Deno.test("fluid type - relativeTo picks the unit of both representations", () => { +test("fluid type - relativeTo picks the unit of both representations", () => { for (const [relativeTo, unit] of [ [undefined, "vw"], ["viewport-width", "vw"], @@ -313,27 +300,30 @@ Deno.test("fluid type - relativeTo picks the unit of both representations", () = ["container", "cqi"], ] as const) { const css = declarations(generateCSS(scale({ ...example, relativeTo }))); - assert( + expect( css .get("--typography_fluid-body-pow-progress") ?.startsWith(`clamp(0rem, (100${unit} - `), css.get("--typography_fluid-body-pow-progress"), - ); + ).toBeTruthy(); for (const label of ["xl", "xs"]) { const clamp = css.get(`--typography_fluid-body-${label}`); - assert(clamp?.includes(`${unit}, `), clamp); + expect(clamp?.includes(`${unit}, `), clamp).toBeTruthy(); } } - const invalid = assertThrows(() => + const invalid = thrownError(() => generateJSON( scale({ ...example, relativeTo: "page" } as unknown as FluidScale["value"]), ), ); - assert(invalid.message.includes('"typography_fluid.body.relativeTo"'), invalid.message); + expect( + invalid.message.includes('"typography_fluid.body.relativeTo"'), + invalid.message, + ).toBeTruthy(); }); -Deno.test("fluid type - two scales never share a pow name", () => { +test("fluid type - two scales never share a pow name", () => { const css = declarations( generateCSS({ typography: { @@ -345,11 +335,11 @@ Deno.test("fluid type - two scales never share a pow name", () => { }), ); - assertEquals(css.get("--typography_fluid-body-pow-min-width"), "20"); - assertEquals(css.get("--typography_fluid-display-big-pow-min-width"), "20"); + expect(css.get("--typography_fluid-body-pow-min-width")).toEqual("20"); + expect(css.get("--typography_fluid-display-big-pow-min-width")).toEqual("20"); }); -Deno.test("fluid type checks - each step is checked once, at its canonical sizes", () => { +test("fluid type checks - each step is checked once, at its canonical sizes", () => { // Step xs is 17.28 / 1.2^2 = 12px at the narrow end. At the wide end it is // 20 / 1.2^2 = 13.89px, where utopia's own scale would give 20 / 1.6^2 = 7.81px. const value = { @@ -360,16 +350,15 @@ Deno.test("fluid type checks - each step is checked once, at its canonical sizes negativeSteps: 2, }; - assertEquals(getDiagnostics(scale(value)), []); + expect(getDiagnostics(scale(value))).toEqual([]); const floor = getDiagnostics(scale(value, { minLegibleSize: 13 })); - assertEquals( - floor.map(({ code, path }) => [code, path]), - [["typography-below-legibility-floor", "typography_fluid.body@xs"]], - ); - assert(floor[0]?.message.includes("12px"), floor[0]?.message); + expect(floor.map(({ code, path }) => [code, path])).toEqual([ + ["typography-below-legibility-floor", "typography_fluid.body@xs"], + ]); + expect(floor[0]?.message.includes("12px"), floor[0]?.message).toBeTruthy(); }); -Deno.test("fluid type checks - a static scale warns once and names no CSS function", () => { +test("fluid type checks - a static scale warns once and names no CSS function", () => { const flat = { ...example, maxFontSize: 18.5, @@ -378,10 +367,12 @@ Deno.test("fluid type checks - a static scale warns once and names no CSS functi negativeSteps: 0, }; const diagnostics = getDiagnostics(scale(flat)); - assertEquals( - diagnostics.map(({ code, path }) => [code, path]), - [["typography-static-scale", "typography_fluid.body"]], - ); + expect(diagnostics.map(({ code, path }) => [code, path])).toEqual([ + ["typography-static-scale", "typography_fluid.body"], + ]); const message = diagnostics[0]?.message ?? ""; - assert(!message.includes("clamp()") && !message.includes("pow()"), message); + expect( + !message.includes("clamp()") && !message.includes("pow()"), + message, + ).toBeTruthy(); }); diff --git a/packages/cssforge/tests/fluid-type-checks.test.ts b/packages/cssforge/tests/fluid-type-checks.test.ts index 870f0b9..20e039d 100644 --- a/packages/cssforge/tests/fluid-type-checks.test.ts +++ b/packages/cssforge/tests/fluid-type-checks.test.ts @@ -1,6 +1,7 @@ +import { expect, test } from "vitest"; import type { CSSForgeConfig, Diagnostic } from "../src/mod.ts"; import { generateCSS, getDiagnostics } from "../src/mod.ts"; -import { assert, assertEquals, assertThrows, Deno } from "./vitest-compat.ts"; +import { thrownError } from "./helpers.ts"; type FluidSettings = NonNullable< NonNullable[string]["settings"] @@ -27,15 +28,15 @@ const typography = ( const codes = (diagnostics: Diagnostic[]) => diagnostics.map(({ code }) => code); -Deno.test("fluid type - a step growing exactly 2.5x passes", () => { +test("fluid type - a step growing exactly 2.5x passes", () => { const config = typography(singleStep(16, 40)); generateCSS(config); - assertEquals(getDiagnostics(config), []); + expect(getDiagnostics(config)).toEqual([]); }); -Deno.test("fluid type - a step growing more than 2.5x is rejected (WCAG 1.4.4)", () => { - const error = assertThrows(() => generateCSS(typography(singleStep(16, 40.01)))); +test("fluid type - a step growing more than 2.5x is rejected (WCAG 1.4.4)", () => { + const error = thrownError(() => generateCSS(typography(singleStep(16, 40.01)))); for (const part of [ '"typography_fluid.body"', @@ -47,12 +48,15 @@ Deno.test("fluid type - a step growing more than 2.5x is rejected (WCAG 1.4.4)", "maxFontSize", "positiveSteps", ]) { - assert(error.message.includes(part), `Expected "${part}" in: ${error.message}`); + expect( + error.message.includes(part), + `Expected "${part}" in: ${error.message}`, + ).toBeTruthy(); } }); -Deno.test("fluid type - the 2.5x error names the step that exceeds it", () => { - const error = assertThrows(() => +test("fluid type - the 2.5x error names the step that exceeds it", () => { + const error = thrownError(() => generateCSS({ typography: { fluid: { @@ -70,23 +74,23 @@ Deno.test("fluid type - the 2.5x error names the step that exceeds it", () => { ); // Step 3 ("2xl") grows from 16 * 1.2^3 = 27.648px to 20 * 1.6^3 = 81.92px, 2.963x; step 2 grows 2.22x. - assert( + expect( error.message.includes('"2xl"') && error.message.includes("27.65px"), error.message, - ); + ).toBeTruthy(); }); -Deno.test("fluid type - a scale growing exactly 10% is fluid", () => { - assertEquals(getDiagnostics(typography(singleStep(20, 22))), []); +test("fluid type - a scale growing exactly 10% is fluid", () => { + expect(getDiagnostics(typography(singleStep(20, 22)))).toEqual([]); }); -Deno.test("fluid type - a scale growing less than 10% at every step warns once", () => { +test("fluid type - a scale growing less than 10% at every step warns once", () => { const [diagnostic, ...rest] = getDiagnostics(typography(singleStep(20, 21.9))); - assertEquals(rest, []); - assertEquals(diagnostic?.code, "typography-static-scale"); - assertEquals(diagnostic?.severity, "warning"); - assertEquals(diagnostic?.path, "typography_fluid.body"); - assert(diagnostic?.message.includes("9.5%"), diagnostic?.message); + expect(rest).toEqual([]); + expect(diagnostic?.code).toEqual("typography-static-scale"); + expect(diagnostic?.severity).toEqual("warning"); + expect(diagnostic?.path).toEqual("typography_fluid.body"); + expect(diagnostic?.message.includes("9.5%"), diagnostic?.message).toBeTruthy(); const staticSteps = getDiagnostics({ typography: { @@ -102,10 +106,10 @@ Deno.test("fluid type - a scale growing less than 10% at every step warns once", }, }, }); - assertEquals(codes(staticSteps), ["typography-static-scale"]); + expect(codes(staticSteps)).toEqual(["typography-static-scale"]); }); -Deno.test("fluid type - one static step in a fluid scale does not warn", () => { +test("fluid type - one static step in a fluid scale does not warn", () => { // Step m stays at 16px while l and xl grow 11% and 23%. const diagnostics = getDiagnostics({ typography: { @@ -122,29 +126,29 @@ Deno.test("fluid type - one static step in a fluid scale does not warn", () => { }, }); - assertEquals(diagnostics, []); + expect(diagnostics).toEqual([]); }); -Deno.test("fluid type - a minimum of exactly 12px is legible", () => { - assertEquals(getDiagnostics(typography(singleStep(12, 16))), []); +test("fluid type - a minimum of exactly 12px is legible", () => { + expect(getDiagnostics(typography(singleStep(12, 16)))).toEqual([]); }); -Deno.test("fluid type - a minimum below 12px warns on the step", () => { +test("fluid type - a minimum below 12px warns on the step", () => { const [diagnostic, ...rest] = getDiagnostics(typography(singleStep(11.99, 16))); - assertEquals(rest, []); - assertEquals(diagnostic?.code, "typography-below-legibility-floor"); - assertEquals(diagnostic?.severity, "warning"); - assertEquals(diagnostic?.path, "typography_fluid.body@m"); - assert( + expect(rest).toEqual([]); + expect(diagnostic?.code).toEqual("typography-below-legibility-floor"); + expect(diagnostic?.severity).toEqual("warning"); + expect(diagnostic?.path).toEqual("typography_fluid.body@m"); + expect( diagnostic?.message.includes("11.99px") && diagnostic.message.includes("12px") && diagnostic.message.includes("minLegibleSize"), diagnostic?.message, - ); + ).toBeTruthy(); }); -Deno.test("fluid type - a shrinking negative step is checked at its smaller end", () => { +test("fluid type - a shrinking negative step is checked at its smaller end", () => { // The scale shrinks from 20px to 16px, so step xs is 20 / 1.2^2 = 13.89px at // 320px and 16 / 1.2^2 = 11.11px at 1280px. const diagnostics = getDiagnostics({ @@ -162,50 +166,49 @@ Deno.test("fluid type - a shrinking negative step is checked at its smaller end" }, }); - assertEquals( - diagnostics.map(({ code, path }) => ({ code, path })), - [{ code: "typography-below-legibility-floor", path: "typography_fluid.body@xs" }], - ); - assert(diagnostics[0]?.message.includes("11.11px"), diagnostics[0]?.message); + expect(diagnostics.map(({ code, path }) => ({ code, path }))).toEqual([ + { code: "typography-below-legibility-floor", path: "typography_fluid.body@xs" }, + ]); + expect( + diagnostics[0]?.message.includes("11.11px"), + diagnostics[0]?.message, + ).toBeTruthy(); }); -Deno.test("fluid type - settings.minLegibleSize moves or disables the floor", () => { - assertEquals( +test("fluid type - settings.minLegibleSize moves or disables the floor", () => { + expect( codes(getDiagnostics(typography(singleStep(13, 16), { minLegibleSize: 14 }))), - ["typography-below-legibility-floor"], - ); - assertEquals( + ).toEqual(["typography-below-legibility-floor"]); + expect( getDiagnostics(typography(singleStep(8, 16), { minLegibleSize: false })), - [], - ); + ).toEqual([]); }); -Deno.test("fluid type - rejects a minLegibleSize that is not a positive number or false", () => { +test("fluid type - rejects a minLegibleSize that is not a positive number or false", () => { for (const minLegibleSize of ["12px", true, 0, -1, Number.NaN]) { const config = typography(singleStep(16, 20), { minLegibleSize, } as unknown as FluidSettings); - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"typography_fluid.body.settings.minLegibleSize"'), error.message, - ); + ).toBeTruthy(); } }); -Deno.test("fluid type - diagnostics leave the generated CSS unchanged", () => { +test("fluid type - diagnostics leave the generated CSS unchanged", () => { const config = typography(singleStep(8, 16)); const css = generateCSS(config); - assertEquals(codes(getDiagnostics(config)), ["typography-below-legibility-floor"]); - assertEquals( - css, + expect(codes(getDiagnostics(config))).toEqual(["typography-below-legibility-floor"]); + expect(css).toEqual( generateCSS(typography(singleStep(8, 16), { minLegibleSize: false })), ); }); -Deno.test("fluid type - rejects a step whose size is 0px", () => { - const error = assertThrows(() => generateCSS(typography(singleStep(0, 16)))); - assert(error.message.includes("greater than 0px"), error.message); +test("fluid type - rejects a step whose size is 0px", () => { + const error = thrownError(() => generateCSS(typography(singleStep(0, 16)))); + expect(error.message.includes("greater than 0px"), error.message).toBeTruthy(); }); diff --git a/packages/cssforge/tests/generator.test.ts b/packages/cssforge/tests/generator.test.ts index 61a08ae..fceec1b 100644 --- a/packages/cssforge/tests/generator.test.ts +++ b/packages/cssforge/tests/generator.test.ts @@ -4,11 +4,11 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; import { build } from "../src/cli.ts"; import { defineConfig, generateStyleDictionaryJSON } from "../src/mod.ts"; -import { assertEquals, assertSnapshot, Deno } from "./vitest-compat.ts"; -Deno.test("generateStyleDictionaryJSON - preserves references and resolved values", async (t) => { +test("generateStyleDictionaryJSON - preserves references and resolved values", () => { const config = defineConfig({ colors: { palette: { @@ -67,39 +67,34 @@ Deno.test("generateStyleDictionaryJSON - preserves references and resolved value generateStyleDictionaryJSON(config, { valueMode: "css-reference" }), ); - assertEquals(result.palette.neutral["900"].value, "var(--palette-neutral-900)"); - assertEquals(result.palette.neutral["900"].type, "color"); - assertEquals(result.palette.neutral["900"].$tier, "primitive"); - assertEquals( - result.theme.light.content.primary.value, + expect(result.palette.neutral["900"].value).toEqual("var(--palette-neutral-900)"); + expect(result.palette.neutral["900"].type).toEqual("color"); + expect(result.palette.neutral["900"].$tier).toEqual("primitive"); + expect(result.theme.light.content.primary.value).toEqual( "var(--theme-light-content-primary)", ); - assertEquals(result.theme.light.content.primary.$tier, "semantic"); - assertEquals(result.theme.light.content.primary.$reference, "palette.neutral.900"); - assertEquals( - result.theme.light.content.primary.$resolvedValue, + expect(result.theme.light.content.primary.$tier).toEqual("semantic"); + expect(result.theme.light.content.primary.$reference).toEqual("palette.neutral.900"); + expect(result.theme.light.content.primary.$resolvedValue).toEqual( "oklch(17.764% 0 none)", ); - assertEquals( - result.theme.light.content.primary.attributes.cssVariable, + expect(result.theme.light.content.primary.attributes.cssVariable).toEqual( "--theme-light-content-primary", ); - assertEquals(result.primitives.button.default.color.attributes.referencePaths, [ + expect(result.primitives.button.default.color.attributes.referencePaths).toEqual([ "theme.light.content.primary", ]); - assertEquals( - result.primitives.button.default.color.attributes.resolvedValue, + expect(result.primitives.button.default.color.attributes.resolvedValue).toEqual( "oklch(17.764% 0 none)", ); - assertEquals( - result.primitives.button.default.padding.attributes.resolvedValue, + expect(result.primitives.button.default.padding.attributes.resolvedValue).toEqual( "0.5rem", ); - await assertSnapshot(t, result); + expect(result).toMatchSnapshot(); }); -Deno.test("generateStyleDictionaryJSON - uses resolved values by default", () => { +test("generateStyleDictionaryJSON - uses resolved values by default", () => { const config = defineConfig({ spacing: { custom: { @@ -114,14 +109,13 @@ Deno.test("generateStyleDictionaryJSON - uses resolved values by default", () => const result = JSON.parse(generateStyleDictionaryJSON(config)); - assertEquals(result.spacing.custom.size["2"].value, "0.5rem"); - assertEquals( - result.spacing.custom.size["2"].attributes.cssVariableReference, + expect(result.spacing.custom.size["2"].value).toEqual("0.5rem"); + expect(result.spacing.custom.size["2"].attributes.cssVariableReference).toEqual( "var(--spacing-size-2)", ); }); -Deno.test("generateStyleDictionaryJSON - resolves hyphenated CSSForge aliases", () => { +test("generateStyleDictionaryJSON - resolves hyphenated CSSForge aliases", () => { const config = defineConfig({ colors: { palette: { @@ -152,13 +146,13 @@ Deno.test("generateStyleDictionaryJSON - resolves hyphenated CSSForge aliases", generateStyleDictionaryJSON(config, { valueMode: "resolved" }), ); - assertEquals(result.primitives.button.default.color.value, "oklch(17.764% 0 none)"); - assertEquals(result.primitives.button.default.color.attributes.referencePaths, [ + expect(result.primitives.button.default.color.value).toEqual("oklch(17.764% 0 none)"); + expect(result.primitives.button.default.color.attributes.referencePaths).toEqual([ "palette.neutral.900", ]); }); -Deno.test("generateStyleDictionaryJSON - preserves cycles and unresolved custom properties", () => { +test("generateStyleDictionaryJSON - preserves cycles and unresolved custom properties", () => { const config = defineConfig({ colors: { palette: { value: {} }, @@ -183,12 +177,12 @@ Deno.test("generateStyleDictionaryJSON - preserves cycles and unresolved custom generateStyleDictionaryJSON(config, { valueMode: "resolved" }), ); - assertEquals(result.theme.light.content.first.value, "var(--second)"); - assertEquals(result.theme.light.content.second.value, "var(--first)"); - assertEquals(result.theme.light.content.external.value, "var(--external-property)"); + expect(result.theme.light.content.first.value).toEqual("var(--second)"); + expect(result.theme.light.content.second.value).toEqual("var(--first)"); + expect(result.theme.light.content.external.value).toEqual("var(--external-property)"); }); -Deno.test("generateStyleDictionaryJSON - uses consistent Style Dictionary paths", () => { +test("generateStyleDictionaryJSON - uses consistent Style Dictionary paths", () => { const config = defineConfig({ typography: { fluid: { @@ -223,24 +217,21 @@ Deno.test("generateStyleDictionaryJSON - uses consistent Style Dictionary paths" const result = JSON.parse(generateStyleDictionaryJSON(config)); - assertEquals( - result["typography-fluid"].app.detail.attributes.sourcePath, + expect(result["typography-fluid"].app.detail.attributes.sourcePath).toEqual( "typography-fluid.app.detail", ); - assertEquals( - result.primitives["text-role"].detail.fontSize.attributes.sourcePath, + expect(result.primitives["text-role"].detail.fontSize.attributes.sourcePath).toEqual( "primitives.text-role.detail.fontSize", ); - assertEquals(result.primitives["text-role"].detail.fontSize.attributes.referencePaths, [ - "typography-fluid.app.detail", - ]); - assertEquals( - result.primitives["text-role"].detail.fontSize.$reference, + expect( + result.primitives["text-role"].detail.fontSize.attributes.referencePaths, + ).toEqual(["typography-fluid.app.detail"]); + expect(result.primitives["text-role"].detail.fontSize.$reference).toEqual( "typography-fluid.app.detail", ); }); -Deno.test("generateStyleDictionaryJSON - anchors tokens for var() usage matching", () => { +test("generateStyleDictionaryJSON - anchors tokens for var() usage matching", () => { const config = defineConfig({ colors: { theme: { @@ -264,12 +255,12 @@ Deno.test("generateStyleDictionaryJSON - anchors tokens for var() usage matching const result = JSON.parse(generateStyleDictionaryJSON(config)); const token = result.theme.light.content.primary; - assertEquals(token.attributes.cssVariable, "--theme-light-content-primary"); - assertEquals(token.attributes.tailwindVariable, token.attributes.cssVariable); - assertEquals(token.value, "oklch(17.764% 0 none)"); + expect(token.attributes.cssVariable).toEqual("--theme-light-content-primary"); + expect(token.attributes.tailwindVariable).toEqual(token.attributes.cssVariable); + expect(token.value).toEqual("oklch(17.764% 0 none)"); }); -Deno.test("generateStyleDictionaryJSON - types tokens by value kind when the module type is coarse", () => { +test("generateStyleDictionaryJSON - types tokens by value kind when the module type is coarse", () => { const config = defineConfig({ typography: { fluid: { @@ -323,22 +314,21 @@ Deno.test("generateStyleDictionaryJSON - types tokens by value kind when the mod const result = JSON.parse(generateStyleDictionaryJSON(config)); - assertEquals(result.primitives["text-role"].detail.fontSize.type, "fontSize"); - assertEquals(result.primitives["text-role"].detail.lineHeight.type, "lineHeight"); - assertEquals( - result.primitives["text-role"].detail["border-radius"].type, + expect(result.primitives["text-role"].detail.fontSize.type).toEqual("fontSize"); + expect(result.primitives["text-role"].detail.lineHeight.type).toEqual("lineHeight"); + expect(result.primitives["text-role"].detail["border-radius"].type).toEqual( "borderRadius", ); - assertEquals(result.primitives["text-role"].detail["box-shadow"].type, "shadow"); - assertEquals(result.primitives["text-role"].detail.fontSize.attributes.referencePaths, [ - "typography-fluid.app.detail", - ]); - assertEquals(result["typography-fluid"].app.detail.type, "typography"); - assertEquals(result.theme.light.content.primary.type, "color"); - assertEquals(result.spacing.custom.size["4"].type, "spacing"); + expect(result.primitives["text-role"].detail["box-shadow"].type).toEqual("shadow"); + expect( + result.primitives["text-role"].detail.fontSize.attributes.referencePaths, + ).toEqual(["typography-fluid.app.detail"]); + expect(result["typography-fluid"].app.detail.type).toEqual("typography"); + expect(result.theme.light.content.primary.type).toEqual("color"); + expect(result.spacing.custom.size["4"].type).toEqual("spacing"); }); -Deno.test("generateStyleDictionaryJSON - keeps the module type when the value contradicts the name", () => { +test("generateStyleDictionaryJSON - keeps the module type when the value contradicts the name", () => { const config = defineConfig({ colors: { palette: { value: { neutral: { 900: "#111" } } }, @@ -366,16 +356,16 @@ Deno.test("generateStyleDictionaryJSON - keeps the module type when the value co const control = result.primitives.control.rounded; // Every leaf name matches a rule, but no value has the expected shape. - assertEquals(control.radius.value, "oklch(17.764% 0 none)"); - assertEquals(control.radius.type, "component"); - assertEquals(control["font-family"].type, "component"); - assertEquals(control.opacity.type, "component"); - assertEquals(control["z-index"].type, "component"); - assertEquals(control.gap.type, "component"); - assertEquals(control["letter-spacing"].type, "component"); + expect(control.radius.value).toEqual("oklch(17.764% 0 none)"); + expect(control.radius.type).toEqual("component"); + expect(control["font-family"].type).toEqual("component"); + expect(control.opacity.type).toEqual("component"); + expect(control["z-index"].type).toEqual("component"); + expect(control.gap.type).toEqual("component"); + expect(control["letter-spacing"].type).toEqual("component"); }); -Deno.test("generateStyleDictionaryJSON - narrows every value kind the file documents", () => { +test("generateStyleDictionaryJSON - narrows every value kind the file documents", () => { const config = defineConfig({ primitives: { type: { @@ -400,17 +390,17 @@ Deno.test("generateStyleDictionaryJSON - narrows every value kind the file docum const result = JSON.parse(generateStyleDictionaryJSON(config)); const display = result.primitives.type.display; - assertEquals(display["font-family"].type, "fontFamily"); - assertEquals(display["font-weight"].type, "fontWeight"); - assertEquals(display.opacity.type, "opacity"); - assertEquals(display["z-index"].type, "zIndex"); - assertEquals(display["letter-spacing"].type, "letterSpacing"); - assertEquals(display["border-radius"].type, "borderRadius"); - assertEquals(display.duration.type, "number"); - assertEquals(display.delay.type, "number"); + expect(display["font-family"].type).toEqual("fontFamily"); + expect(display["font-weight"].type).toEqual("fontWeight"); + expect(display.opacity.type).toEqual("opacity"); + expect(display["z-index"].type).toEqual("zIndex"); + expect(display["letter-spacing"].type).toEqual("letterSpacing"); + expect(display["border-radius"].type).toEqual("borderRadius"); + expect(display.duration.type).toEqual("number"); + expect(display.delay.type).toEqual("number"); }); -Deno.test("generateStyleDictionaryJSON - every token carries the fields consumers rely on", () => { +test("generateStyleDictionaryJSON - every token carries the fields consumers rely on", () => { const config = defineConfig({ colors: { palette: { value: { neutral: { 900: "#111" } } }, @@ -441,29 +431,27 @@ Deno.test("generateStyleDictionaryJSON - every token carries the fields consumer }; collect(result); - assertEquals(tokens.length, 3); + expect(tokens.length).toEqual(3); for (const [tokenPath, token] of tokens) { - assertEquals(typeof token.type, "string", `${tokenPath} has a type`); - assertEquals( + expect(typeof token.type, `${tokenPath} has a type`).toEqual("string"); + expect( token.attributes.tailwindVariable, - token.attributes.cssVariable, `${tokenPath} anchors its CSS variable`, - ); + ).toEqual(token.attributes.cssVariable); if (token.$reference) { const referenced = tokens.find(([candidate]) => candidate === token.$reference); - assertEquals(referenced !== undefined, true, `${tokenPath} reference resolves`); + expect(referenced !== undefined, `${tokenPath} reference resolves`).toEqual(true); } for (const referencePath of token.attributes.referencePaths ?? []) { - assertEquals( + expect( tokens.some(([candidate]) => candidate === referencePath), - true, `${tokenPath} reference path ${referencePath} resolves`, - ); + ).toEqual(true); } } }); -Deno.test("generateStyleDictionaryJSON - rejects token paths that collide after normalization", () => { +test("generateStyleDictionaryJSON - rejects token paths that collide after normalization", () => { const config = defineConfig({ colors: { palette: { @@ -482,13 +470,12 @@ Deno.test("generateStyleDictionaryJSON - rejects token paths that collide after error = caught; } - assertEquals( - error instanceof Error ? error.message : "", + expect(error instanceof Error ? error.message : "").toEqual( 'Token path collision after normalization: "palette.cosmicGold.50" and "palette.cosmic-gold.50" both become "palette.cosmic-gold.50"', ); }); -Deno.test("generateStyleDictionaryJSON - supports legacy value-wrapper reference paths", () => { +test("generateStyleDictionaryJSON - supports legacy value-wrapper reference paths", () => { const config = defineConfig({ colors: { palette: { @@ -574,26 +561,23 @@ Deno.test("generateStyleDictionaryJSON - supports legacy value-wrapper reference const result = JSON.parse(generateStyleDictionaryJSON(config)); - assertEquals( - result.gradients["gold-gradient"].primary.$reference, + expect(result.gradients["gold-gradient"].primary.$reference).toEqual( "palette.cosmic-gold.50", ); - assertEquals(result.gradients["gold-gradient"].primary.attributes.referencePaths, [ + expect(result.gradients["gold-gradient"].primary.attributes.referencePaths).toEqual([ "palette.cosmic-gold.50", "palette.cosmic-gold.90", ]); - assertEquals(result.theme.light.content.primary.$reference, "palette.gray.950"); - assertEquals( - result.primitives.button.default.fontWeight.attributes.resolvedValue, + expect(result.theme.light.content.primary.$reference).toEqual("palette.gray.950"); + expect(result.primitives.button.default.fontWeight.attributes.resolvedValue).toEqual( "400", ); - assertEquals( - result.primitives.button.default.padding.attributes.resolvedValue, + expect(result.primitives.button.default.padding.attributes.resolvedValue).toEqual( "0.5rem", ); }); -Deno.test("cli - style-dictionary and all modes write their declared outputs", async () => { +test("cli - style-dictionary and all modes write their declared outputs", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-style-dictionary-")); try { @@ -641,13 +625,15 @@ Deno.test("cli - style-dictionary and all modes write their declared outputs", a const output = JSON.parse(await readFile(styleDictionaryOutput, "utf8")); - assertEquals(result.status, 0); - assertEquals(result.stderr, ""); - assertEquals(result.stdout.includes("Generated Style Dictionary JSON written"), true); - assertEquals(output.spacing.custom.size["2"].value, "0.5rem"); - assertEquals(existsSync(cssOutput), false); - assertEquals(existsSync(jsonOutput), false); - assertEquals(existsSync(tsOutput), false); + expect(result.status).toEqual(0); + expect(result.stderr).toEqual(""); + expect(result.stdout.includes("Generated Style Dictionary JSON written")).toEqual( + true, + ); + expect(output.spacing.custom.size["2"].value).toEqual("0.5rem"); + expect(existsSync(cssOutput)).toEqual(false); + expect(existsSync(jsonOutput)).toEqual(false); + expect(existsSync(tsOutput)).toEqual(false); const allResult = spawnSync( process.execPath, @@ -670,13 +656,13 @@ Deno.test("cli - style-dictionary and all modes write their declared outputs", a ); const legacyJson = JSON.parse(await readFile(jsonOutput, "utf8")); - assertEquals(allResult.status, 0); - assertEquals(allResult.stderr, ""); - assertEquals(existsSync(cssOutput), true); - assertEquals(existsSync(jsonOutput), true); - assertEquals(existsSync(tsOutput), true); - assertEquals(existsSync(styleDictionaryOutput), true); - assertEquals(Object.keys(legacyJson.spacing.custom.size["2"]).sort(), [ + expect(allResult.status).toEqual(0); + expect(allResult.stderr).toEqual(""); + expect(existsSync(cssOutput)).toEqual(true); + expect(existsSync(jsonOutput)).toEqual(true); + expect(existsSync(tsOutput)).toEqual(true); + expect(existsSync(styleDictionaryOutput)).toEqual(true); + expect(Object.keys(legacyJson.spacing.custom.size["2"]).sort()).toEqual([ "key", "value", "variable", @@ -686,7 +672,7 @@ Deno.test("cli - style-dictionary and all modes write their declared outputs", a } }); -Deno.test("cli - style-dictionary mode can emit CSS variables for usage matching", async () => { +test("cli - style-dictionary mode can emit CSS variables for usage matching", async () => { const tempDir = await mkdtemp( join(tmpdir(), "cssforge-style-dictionary-css-reference-"), ); @@ -723,15 +709,15 @@ Deno.test("cli - style-dictionary mode can emit CSS variables for usage matching const output = JSON.parse(await readFile(styleDictionaryOutput, "utf8")); - assertEquals(result.status, 0); - assertEquals(result.stderr, ""); - assertEquals(output.spacing.custom.size["2"].value, "var(--spacing-size-2)"); + expect(result.status).toEqual(0); + expect(result.stderr).toEqual(""); + expect(output.spacing.custom.size["2"].value).toEqual("var(--spacing-size-2)"); } finally { await rm(tempDir, { recursive: true, force: true }); } }); -Deno.test("build - legacy output modes do not require a Style Dictionary path", async () => { +test("build - legacy output modes do not require a Style Dictionary path", async () => { const tempDir = await mkdtemp(join(tmpdir(), "cssforge-legacy-build-")); try { @@ -755,8 +741,8 @@ Deno.test("build - legacy output modes do not require a Style Dictionary path", tsOutput: join(tempDir, "output.ts"), }); - assertEquals(result.success, true); - assertEquals(existsSync(cssOutput), true); + expect(result.success).toEqual(true); + expect(existsSync(cssOutput)).toEqual(true); } finally { await rm(tempDir, { recursive: true, force: true }); } diff --git a/packages/cssforge/tests/helpers.ts b/packages/cssforge/tests/helpers.ts index a2dd9c5..a71e1ec 100644 --- a/packages/cssforge/tests/helpers.ts +++ b/packages/cssforge/tests/helpers.ts @@ -1,3 +1,4 @@ +import { expect } from "vitest"; import type { Output } from "../src/lib.ts"; /** @@ -16,3 +17,15 @@ export const combine = (css: Output["css"]) => export const lineHasProp = (lines: string[]) => (prop: string) => lines.some((line) => line.includes(prop)); + +/** Asserts that `run` throws and returns the Error so the caller can inspect it. */ +export const thrownError = (run: () => unknown): Error => { + try { + run(); + } catch (error) { + expect(error).toBeInstanceOf(Error); + return error as Error; + } + + throw new Error("Expected the call to throw, but it returned a value."); +}; diff --git a/packages/cssforge/tests/identifier-validation.test.ts b/packages/cssforge/tests/identifier-validation.test.ts index 29ca3fb..87ee9d2 100644 --- a/packages/cssforge/tests/identifier-validation.test.ts +++ b/packages/cssforge/tests/identifier-validation.test.ts @@ -1,10 +1,10 @@ +import { expect, test } from "vitest"; import { generateCSS, generateJSON, generateTS } from "../src/generator.ts"; import { defineConfig } from "../src/mod.ts"; import { processColors } from "../src/modules/colors.ts"; import { processPrimitives } from "../src/modules/primitive.ts"; import { processSpacing } from "../src/modules/spacing.ts"; import { processTypography } from "../src/modules/typography.ts"; -import { assert, assertEquals, Deno } from "./vitest-compat.ts"; /** * Declaration names that are not valid CSS dashed identifiers. The name is read @@ -42,13 +42,13 @@ const fluidSize = { negativeSteps: 0, }; -Deno.test("invalidDashedIdentifiers - detects the malformed name from issue #29", () => { - assertEquals(invalidDashedIdentifiers("--card button-default-gap: 1rem;"), [ +test("invalidDashedIdentifiers - detects the malformed name from issue #29", () => { + expect(invalidDashedIdentifiers("--card button-default-gap: 1rem;")).toEqual([ "--card button-default-gap", ]); - assertEquals(invalidDashedIdentifiers("--palette-coral-50: oklch(0 0 0);"), []); - assertEquals(invalidDashedIdentifiers("---card-default-gap: 1rem;"), []); - assertEquals(invalidDashedIdentifiers("--50-default-gap: 1rem;"), []); + expect(invalidDashedIdentifiers("--palette-coral-50: oklch(0 0 0);")).toEqual([]); + expect(invalidDashedIdentifiers("---card-default-gap: 1rem;")).toEqual([]); + expect(invalidDashedIdentifiers("--50-default-gap: 1rem;")).toEqual([]); }); /** @@ -247,31 +247,31 @@ const rejectedSegments: Array<[label: string, run: () => unknown, expected: stri ], ]; -Deno.test("validateName - rejects an invalid segment at every call site", () => { +test("validateName - rejects an invalid segment at every call site", () => { for (const [label, run, expectedPath] of rejectedSegments) { const message = captureError(run); - assert( + expect( message.includes(expectedPath), `${label} must name "${expectedPath}". Received: ${message}`, - ); + ).toBeTruthy(); } }); -Deno.test("validateName - rejects every ASCII delimiter as a name segment", () => { +test("validateName - rejects every ASCII delimiter as a name segment", () => { for (const character of " !\"#$%&'()*+,./:;<=>?@[\\]^`{|}~") { const message = captureError(() => processSpacing({ custom: { size: { value: { [`a${character}b`]: "4px" } } }, } as never), ); - assert( + expect( message.includes(`a${character}b`), `Expected "${character}" to be rejected. Received: ${message}`, - ); + ).toBeTruthy(); } }); -Deno.test("validateName - rejects an empty alias key that would emit var(--)", () => { +test("validateName - rejects an empty alias key that would emit var(--)", () => { const message = captureError(() => processPrimitives({ primitives: { @@ -283,13 +283,13 @@ Deno.test("validateName - rejects an empty alias key that would emit var(--)", ( }, } as never), ); - assert( + expect( message.includes("primitives.btn.d.variables.") && message.includes("non-empty"), `An empty alias key must be rejected with its path. Received: ${message}`, - ); + ).toBeTruthy(); }); -Deno.test("validateName - names the configured alias key verbatim in the error", () => { +test("validateName - names the configured alias key verbatim in the error", () => { // `getResolvedVariablesMap` builds the reference as `--${varKey}`, so a key // written `--alias` already includes its hyphens and must be quoted as-is. const message = captureError(() => @@ -306,14 +306,14 @@ Deno.test("validateName - names the configured alias key verbatim in the error", }, } as never), ); - assert( + expect( message.includes("Invalid name: --a b") && message.includes("primitives.btn.d.variables.--a b"), `Error must name the configured key verbatim. Received: ${message}`, - ); + ).toBeTruthy(); }); -Deno.test("validateName - keeps alias keys and custom labels that are not token keys", () => { +test("validateName - keeps alias keys and custom labels that are not token keys", () => { // Alias keys and custom labels are not token keys, so the reserved-keyword // rule must not apply to them. const aliasResult = processPrimitives({ @@ -329,8 +329,7 @@ Deno.test("validateName - keeps alias keys and custom labels that are not token }, }, } as never); - assertEquals( - aliasResult.css.root, + expect(aliasResult.css.root).toEqual( "/* btn */\n--btn-d-color: var(--palette-coral-50);", ); @@ -339,7 +338,7 @@ Deno.test("validateName - keeps alias keys and custom labels that are not token base: { value: fluidSize, settings: { customLabel: { "1": "value", "0": "0" } } }, }, } as never); - assertEquals(labelResult.css.root?.includes("--typography_fluid-base-value:"), true); + expect(labelResult.css.root?.includes("--typography_fluid-base-value:")).toEqual(true); // A hyphen-only alias key is a name that already contains the hyphens, so // `var(----)` resolves and must keep working. @@ -353,10 +352,12 @@ Deno.test("validateName - keeps alias keys and custom labels that are not token }, }, } as never); - assertEquals(hyphenKey.css.root, "/* btn */\n--btn-d-color: var(--palette-coral-50);"); + expect(hyphenKey.css.root).toEqual( + "/* btn */\n--btn-d-color: var(--palette-coral-50);", + ); }); -Deno.test("validateName - keeps numeric, hyphen, underscore and non-ASCII names working", () => { +test("validateName - keeps numeric, hyphen, underscore and non-ASCII names working", () => { const config = defineConfig({ spacing: { custom: { @@ -382,12 +383,15 @@ Deno.test("validateName - keeps numeric, hyphen, underscore and non-ASCII names "--card-2-hover_focus-background-color: 1rem;", "--card-2-hover_focus-row-gap: 2rem;", ]) { - assert(css.includes(expected), `Expected "${expected}". Received: ${css}`); + expect( + css.includes(expected), + `Expected "${expected}". Received: ${css}`, + ).toBeTruthy(); } - assertEquals(invalidDashedIdentifiers(css), []); + expect(invalidDashedIdentifiers(css)).toEqual([]); }); -Deno.test("validateName - validates the custom label that is actually emitted", () => { +test("validateName - validates the custom label that is actually emitted", () => { // `customLabel` is read with bracket access, so an inherited mapping resolves // through the prototype chain and must still be validated. const inherited: Record = Object.create({ "1": "big type", "0": "0" }); @@ -396,21 +400,21 @@ Deno.test("validateName - validates the custom label that is actually emitted", fluid: { base: { value: fluidSize, settings: { customLabel: inherited } } }, } as never), ); - assert( + expect( message.includes("big type") && message.includes("typography_fluid.base.settings.customLabel"), `An inherited invalid label must be rejected. Received: ${message}`, - ); + ).toBeTruthy(); // A label with no mapping falls back to the generated step label. const fallback = processTypography({ fluid: { base: { value: fluidSize, settings: { customLabel: {} } } }, } as never); - assertEquals(fallback.css.root?.includes("--typography_fluid-base-1:"), true); - assertEquals(invalidDashedIdentifiers(fallback.css.root ?? ""), []); + expect(fallback.css.root?.includes("--typography_fluid-base-1:")).toEqual(true); + expect(invalidDashedIdentifiers(fallback.css.root ?? "")).toEqual([]); }); -Deno.test("validateName - surfaces name errors that module error handling would swallow", () => { +test("validateName - surfaces name errors that module error handling would swallow", () => { // Palette colors and themes log ordinary token failures and continue, so a // configuration mistake must escape instead of silently dropping declarations. const paletteVariant = captureError(() => @@ -418,7 +422,7 @@ Deno.test("validateName - surfaces name errors that module error handling would palette: { value: { coral: { value: { "50 light": { hex: "#ff0000" } } } } }, } as never), ); - assertEquals(paletteVariant.includes("palette.coral.50 light"), true); + expect(paletteVariant.includes("palette.coral.50 light")).toEqual(true); const themeVariant = captureError(() => processColors({ @@ -435,10 +439,10 @@ Deno.test("validateName - surfaces name errors that module error handling would }, } as never), ); - assertEquals(themeVariant.includes("theme.light.background.primary dark"), true); + expect(themeVariant.includes("theme.light.background.primary dark")).toEqual(true); }); -Deno.test("generateCSS - never emits a malformed custom property name", () => { +test("generateCSS - never emits a malformed custom property name", () => { const config = defineConfig({ colors: { palette: { value: { coral: { value: { "50": { hex: "#FF7E60" } } } } }, @@ -493,14 +497,14 @@ Deno.test("generateCSS - never emits a malformed custom property name", () => { }); const css = generateCSS(config); - assertEquals(invalidDashedIdentifiers(css), []); + expect(invalidDashedIdentifiers(css)).toEqual([]); // Keeps the check above from passing vacuously: it must see the issue #29 name. - assertEquals(invalidDashedIdentifiers(`${css}\n--card button-default-gap: 1rem;`), [ + expect(invalidDashedIdentifiers(`${css}\n--card button-default-gap: 1rem;`)).toEqual([ "--card button-default-gap", ]); }); -Deno.test("generator - rejects an invalid name through every output", () => { +test("generator - rejects an invalid name through every output", () => { const config = defineConfig({ primitives: { "card button": { value: { default: { value: { gap: "1rem" } } } } }, }); @@ -510,9 +514,9 @@ Deno.test("generator - rejects an invalid name through every output", () => { ["JSON", () => generateJSON(config)], ["TypeScript", () => generateTS(config)], ] as const) { - assert( + expect( captureError(run).includes("primitives.card button"), `${output} generation must fail with the configuration path.`, - ); + ).toBeTruthy(); } }); diff --git a/packages/cssforge/tests/invalid-name-error-export.test.ts b/packages/cssforge/tests/invalid-name-error-export.test.ts index ceea354..55fa5de 100644 --- a/packages/cssforge/tests/invalid-name-error-export.test.ts +++ b/packages/cssforge/tests/invalid-name-error-export.test.ts @@ -1,3 +1,4 @@ +import { expect, test } from "vitest"; import { generateCSS, generateStyleDictionaryJSON, @@ -7,21 +8,20 @@ import { processSpacing, processTypography, } from "../src/mod.ts"; -import { assert, assertEquals, Deno } from "./vitest-compat.ts"; // Imported from the package entry on purpose: the export is the behaviour under // test, so importing from `helpers.ts` would pass even without a re-export. -Deno.test("InvalidNameError - is exported from the package entry as a constructor", () => { - assertEquals(typeof InvalidNameError, "function"); - assert( +test("InvalidNameError - is exported from the package entry as a constructor", () => { + expect(typeof InvalidNameError).toEqual("function"); + expect( InvalidNameError.prototype instanceof Error, "InvalidNameError must extend Error so existing catch blocks keep working.", - ); + ).toBeTruthy(); const error = new InvalidNameError("boom"); - assert(error instanceof Error, "An instance must be an Error."); - assertEquals(error.name, "InvalidNameError"); - assertEquals(error.message, "boom"); + expect(error instanceof Error, "An instance must be an Error.").toBeTruthy(); + expect(error.name).toEqual("InvalidNameError"); + expect(error.message).toEqual("boom"); }); const invalidPrimitiveConfig = { @@ -32,7 +32,7 @@ const invalidPrimitiveConfig = { }, }; -Deno.test("InvalidNameError - catches a name validation failure with instanceof", () => { +test("InvalidNameError - catches a name validation failure with instanceof", () => { let caught: unknown; try { processPrimitives(invalidPrimitiveConfig as never); @@ -40,11 +40,14 @@ Deno.test("InvalidNameError - catches a name validation failure with instanceof" caught = error; } - assert(caught instanceof InvalidNameError, "Expected an InvalidNameError instance."); - assertEquals((caught as Error).name, "InvalidNameError"); + expect( + caught instanceof InvalidNameError, + "Expected an InvalidNameError instance.", + ).toBeTruthy(); + expect((caught as Error).name).toEqual("InvalidNameError"); }); -Deno.test("InvalidNameError - is reachable through every public entry", () => { +test("InvalidNameError - is reachable through every public entry", () => { // The entries `mod.ts` exports. `generateJSON` and `generateTS` live in // `generator.ts` and are not part of the package surface. const entries: Array<[string, () => unknown]> = [ @@ -79,14 +82,14 @@ Deno.test("InvalidNameError - is reachable through every public entry", () => { } catch (error) { caught = error; } - assert( + expect( caught instanceof InvalidNameError, `${label} must surface an InvalidNameError, received ${String(caught)}.`, - ); + ).toBeTruthy(); } }); -Deno.test("InvalidNameError - does not affect valid configurations", () => { +test("InvalidNameError - does not affect valid configurations", () => { const css = generateCSS({ primitives: { "card-2": { @@ -95,8 +98,8 @@ Deno.test("InvalidNameError - does not affect valid configurations", () => { }, } as never); - assert( + expect( css.includes("--card-2-default-gap: 1rem;"), `Valid names must keep generating. Received: ${css}`, - ); + ).toBeTruthy(); }); diff --git a/packages/cssforge/tests/key-collision.test.ts b/packages/cssforge/tests/key-collision.test.ts index 867dc67..6ee56d8 100644 --- a/packages/cssforge/tests/key-collision.test.ts +++ b/packages/cssforge/tests/key-collision.test.ts @@ -1,3 +1,4 @@ +import { expect, test } from "vitest"; import { generateCSS, generateJSON, @@ -6,9 +7,7 @@ import { } from "../src/generator.ts"; // The public entry point, so the diagnostic is proven reachable by consumers. import { defineConfig, generateCSS as generateCSSFromPublicEntry } from "../src/mod.ts"; -import { assertDoesNotThrow, assertEquals, assertThrows, Deno } from "./vitest-compat.ts"; - -const captureError = (run: () => unknown): Error => assertThrows(run); +import { thrownError } from "./helpers.ts"; /** `primitives.a-b.c.x` and `primitives.a.b-c.x` both join to `--a-b-c-x`. */ const collidingConfig = () => @@ -24,7 +23,7 @@ const collisionMessage = const palette = { value: { white: { value: { x: { hex: "#ffffff" } } } } }; -Deno.test("generator - every output rejects two paths that generate the same key", () => { +test("generator - every output rejects two paths that generate the same key", () => { const outputs: Array<[string, () => unknown]> = [ ["generateCSS", () => generateCSS(collidingConfig())], ["generateJSON", () => generateJSON(collidingConfig())], @@ -34,11 +33,11 @@ Deno.test("generator - every output rejects two paths that generate the same key ]; for (const [label, run] of outputs) { - assertEquals(captureError(run).message, collisionMessage, label); + expect(thrownError(run).message, label).toEqual(collisionMessage); } }); -Deno.test("generator - a collision is reported from any module", () => { +test("generator - a collision is reported from any module", () => { const collisions: Array<[string, () => unknown, string]> = [ [ "colors", @@ -75,11 +74,11 @@ Deno.test("generator - a collision is reported from any module", () => { ]; for (const [label, run, expected] of collisions) { - assertEquals(captureError(run).message, expected, label); + expect(thrownError(run).message, label).toEqual(expected); } }); -Deno.test("generator - a near-miss with distinct keys is unaffected", () => { +test("generator - a near-miss with distinct keys is unaffected", () => { const config = defineConfig({ primitives: { "a-b": { value: { c: { value: { x: "1rem" } } } }, @@ -87,12 +86,12 @@ Deno.test("generator - a near-miss with distinct keys is unaffected", () => { }, }); - assertEquals(generateCSS(config).includes("--a-b-c-x: 1rem;"), true); - assertEquals(generateCSS(config).includes("--a-d-c-x: 2rem;"), true); + expect(generateCSS(config).includes("--a-b-c-x: 1rem;")).toEqual(true); + expect(generateCSS(config).includes("--a-d-c-x: 2rem;")).toEqual(true); const json = JSON.parse(generateJSON(config)); - assertEquals(json.primitives["a-b"].c.x.key, "--a-b-c-x"); - assertEquals(json.primitives.a["d-c"].x.key, "--a-d-c-x"); + expect(json.primitives["a-b"].c.x.key).toEqual("--a-b-c-x"); + expect(json.primitives.a["d-c"].x.key).toEqual("--a-d-c-x"); }); /** A theme color that emits `--primary` when `variantNameOnly` is set. */ @@ -106,7 +105,7 @@ const variantTheme = ( }, }); -Deno.test("generator - variantNameOnly themes may reuse a key in different scopes", () => { +test("generator - variantNameOnly themes may reuse a key in different scopes", () => { const css = generateCSS( defineConfig({ colors: { @@ -123,12 +122,12 @@ Deno.test("generator - variantNameOnly themes may reuse a key in different scope }), ); - assertEquals((css.match(/--primary:/g) ?? []).length, 3); - assertEquals(css.includes("@media (prefers-color-scheme: dark) {"), true); - assertEquals(css.includes(".ThemePink {"), true); + expect((css.match(/--primary:/g) ?? []).length).toEqual(3); + expect(css.includes("@media (prefers-color-scheme: dark) {")).toEqual(true); + expect(css.includes(".ThemePink {")).toEqual(true); }); -Deno.test("generator - unscoped declarations that share a key are rejected", () => { +test("generator - unscoped declarations that share a key are rejected", () => { // Every case below lands in `:root`, so one declaration overwrites the other. const unscoped: Array<[string, () => unknown, string]> = [ [ @@ -217,12 +216,12 @@ Deno.test("generator - unscoped declarations that share a key are rejected", () ]; for (const [label, run, expected] of unscoped) { - assertEquals(captureError(run).message, expected, label); + expect(thrownError(run).message, label).toEqual(expected); } }); -Deno.test("generator - an explicit :root selector collides inside a shared at-rule", () => { - const error = captureError(() => +test("generator - an explicit :root selector collides inside a shared at-rule", () => { + const error = thrownError(() => generateCSS( defineConfig({ colors: { @@ -244,13 +243,12 @@ Deno.test("generator - an explicit :root selector collides inside a shared at-ru }), ), ); - assertEquals( - error.message, + expect(error.message).toEqual( 'Token key collision: "theme.light.background.primary" and "theme.dark.background.primary" both generate "--primary". Rename one of the configuration paths.', ); }); -Deno.test("generator - differing wrappers may share a key", () => { +test("generator - differing wrappers may share a key", () => { // A wrapper is part of the scope identity, so the same name under two // different wrappers does not overwrite. Covers both modules and both // wrapper kinds. @@ -349,9 +347,9 @@ Deno.test("generator - differing wrappers may share a key", () => { ]; for (const [label, run, key, wrapper] of accepted) { - assertDoesNotThrow(run, label); + expect(run, label).not.toThrow(); const css = run(); - assertEquals((css.match(new RegExp(key, "g")) ?? []).length, 2, label); - assertEquals(css.includes(wrapper), true, label); + expect((css.match(new RegExp(key, "g")) ?? []).length, label).toEqual(2); + expect(css.includes(wrapper), label).toEqual(true); } }); diff --git a/packages/cssforge/tests/lib.test.ts b/packages/cssforge/tests/lib.test.ts index 0914e8d..22e8210 100644 --- a/packages/cssforge/tests/lib.test.ts +++ b/packages/cssforge/tests/lib.test.ts @@ -1,5 +1,5 @@ +import { expect, test } from "vitest"; import { getReferencePaths, resolveValue } from "../src/lib.ts"; -import { assertEquals, Deno } from "./vitest-compat.ts"; const aliasMap = new Map([ ["--surface-muted", "--palette-gray-100"], @@ -15,80 +15,74 @@ const aliasTable = [ ["var( --bg )", "var( --palette-gray-100 )"], ] as const; -Deno.test("resolveValue - resolves the alias table from the issue", () => { +test("resolveValue - resolves the alias table from the issue", () => { for (const [input, expected] of aliasTable) { - assertEquals(resolve(input), expected, input); + expect(resolve(input), input).toEqual(expected); } }); -Deno.test("resolveValue - preserves nested fallback content", () => { - assertEquals( - resolve("var(--surface-muted, var(--bg, red))"), +test("resolveValue - preserves nested fallback content", () => { + expect(resolve("var(--surface-muted, var(--bg, red))")).toEqual( "var(--palette-gray-100, var(--palette-gray-100, red))", ); }); -Deno.test("resolveValue - preserves whitespace and fallback inside var()", () => { - assertEquals(resolve("var( --bg , red )"), "var( --palette-gray-100 , red )"); +test("resolveValue - preserves whitespace and fallback inside var()", () => { + expect(resolve("var( --bg , red )")).toEqual("var( --palette-gray-100 , red )"); }); -Deno.test("resolveValue - resolves every reference in a value", () => { - assertEquals( - resolve("linear-gradient(to right, var(--bg), var(--surface-muted))"), +test("resolveValue - resolves every reference in a value", () => { + expect(resolve("linear-gradient(to right, var(--bg), var(--surface-muted))")).toEqual( "linear-gradient(to right, var(--palette-gray-100), var(--palette-gray-100))", ); - assertEquals( - resolve("calc(var(--bg) + var(--surface-muted))"), + expect(resolve("calc(var(--bg) + var(--surface-muted))")).toEqual( "calc(var(--palette-gray-100) + var(--palette-gray-100))", ); - assertEquals( - resolve("color-mix(in oklch, var(--bg) 50%, var(--surface-muted))"), + expect(resolve("color-mix(in oklch, var(--bg) 50%, var(--surface-muted))")).toEqual( "color-mix(in oklch, var(--palette-gray-100) 50%, var(--palette-gray-100))", ); }); -Deno.test("resolveValue - leaves unmapped external custom properties unchanged", () => { - assertEquals(resolve("var(--external-thing)"), "var(--external-thing)"); - assertEquals( - resolve("var(--external-thing, var(--bg))"), +test("resolveValue - leaves unmapped external custom properties unchanged", () => { + expect(resolve("var(--external-thing)")).toEqual("var(--external-thing)"); + expect(resolve("var(--external-thing, var(--bg))")).toEqual( "var(--external-thing, var(--palette-gray-100))", ); - assertEquals(resolve("var(--brand-color, #fff)"), "var(--brand-color, #fff)"); + expect(resolve("var(--brand-color, #fff)")).toEqual("var(--brand-color, #fff)"); }); -Deno.test("resolveValue - does not rewrite var() text inside a quoted string", () => { - assertEquals(resolve('url("var(--surface-muted)")'), 'url("var(--surface-muted)")'); - assertEquals(resolve("content: 'var(--bg)'"), "content: 'var(--bg)'"); +test("resolveValue - does not rewrite var() text inside a quoted string", () => { + expect(resolve('url("var(--surface-muted)")')).toEqual('url("var(--surface-muted)")'); + expect(resolve("content: 'var(--bg)'")).toEqual("content: 'var(--bg)'"); }); -Deno.test("resolveValue - ignores comments and whitespace around the alias name", () => { - assertEquals(resolve("var(/* alias */--bg)"), "var(/* alias */--palette-gray-100)"); - assertEquals( - resolve("var( /* alias */ --bg )"), +test("resolveValue - ignores comments and whitespace around the alias name", () => { + expect(resolve("var(/* alias */--bg)")).toEqual("var(/* alias */--palette-gray-100)"); + expect(resolve("var( /* alias */ --bg )")).toEqual( "var( /* alias */ --palette-gray-100 )", ); - assertEquals(resolve("var(--bg /* alias */)"), "var(--palette-gray-100 /* alias */)"); + expect(resolve("var(--bg /* alias */)")).toEqual("var(--palette-gray-100 /* alias */)"); }); -Deno.test("resolveValue - leaves malformed var() text unchanged", () => { - assertEquals(resolve("var(--bg"), "var(--bg"); - assertEquals(resolve("var()"), "var()"); - assertEquals(resolve("var(--)"), "var(--)"); - assertEquals(resolve("var(--bg, red"), "var(--bg, red"); +test("resolveValue - leaves malformed var() text unchanged", () => { + expect(resolve("var(--bg")).toEqual("var(--bg"); + expect(resolve("var()")).toEqual("var()"); + expect(resolve("var(--)")).toEqual("var(--)"); + expect(resolve("var(--bg, red")).toEqual("var(--bg, red"); // An unbalanced call must not rewrite references inside it. - assertEquals( - resolve("var(--ext, var(--surface-muted)"), + expect(resolve("var(--ext, var(--surface-muted)")).toEqual( "var(--ext, var(--surface-muted)", ); - assertEquals(resolve("var(--surface-muted, var(--bg"), "var(--surface-muted, var(--bg"); + expect(resolve("var(--surface-muted, var(--bg")).toEqual( + "var(--surface-muted, var(--bg", + ); }); -Deno.test("getReferencePaths - reports aliases used with fallbacks", () => { - assertEquals( +test("getReferencePaths - reports aliases used with fallbacks", () => { + expect( getReferencePaths({ value: "var(--surface-muted, var(--bg))", variables: { bg: "palette.gray.100", "surface-muted": "palette.gray.100" }, }), - ["palette.gray.100"], - ); + ).toEqual(["palette.gray.100"]); }); diff --git a/packages/cssforge/tests/light-dark.test.ts b/packages/cssforge/tests/light-dark.test.ts index 098096f..ab1f159 100644 --- a/packages/cssforge/tests/light-dark.test.ts +++ b/packages/cssforge/tests/light-dark.test.ts @@ -1,7 +1,8 @@ +import { expect, test } from "vitest"; import { generateJSON, generateTS } from "../src/generator.ts"; import type { CSSForgeConfig, LightDarkSettings } from "../src/mod.ts"; import { defineConfig, generateCSS, generateStyleDictionaryJSON } from "../src/mod.ts"; -import { assert, assertEquals, assertThrows, Deno } from "./vitest-compat.ts"; +import { thrownError } from "./helpers.ts"; const palette = { value: { @@ -55,21 +56,20 @@ const withLightDark = (lightDark: unknown) => { }; const throwsAt = (config: unknown, ...fragments: string[]) => { - const error = assertThrows(() => generateCSS(config as CSSForgeConfig)); + const error = thrownError(() => generateCSS(config as CSSForgeConfig)); for (const fragment of fragments) { - assert( + expect( error.message.includes(fragment), `Expected the error to include ${fragment}. Received: ${error.message}`, - ); + ).toBeTruthy(); } }; -Deno.test("generateCSS - lightDark emits one light-dark() token per paired color at :root", () => { +test("generateCSS - lightDark emits one light-dark() token per paired color at :root", () => { const css = generateCSS(pairedConfig()); const themes = css.slice(css.indexOf("/* Themes */")); - assertEquals( - themes, + expect(themes).toEqual( [ "/* Themes */", "/* Theme: light-dark(light, dark) */", @@ -83,7 +83,7 @@ Deno.test("generateCSS - lightDark emits one light-dark() token per paired color ); }); -Deno.test("generateCSS - lightDark colorScheme emits a color-scheme override rule per scheme", () => { +test("generateCSS - lightDark colorScheme emits a color-scheme override rule per scheme", () => { const css = generateCSS( pairedConfig({ light: "light", @@ -92,7 +92,7 @@ Deno.test("generateCSS - lightDark colorScheme emits a color-scheme override rul }), ); - assert( + expect( css.endsWith( [ "}", @@ -105,10 +105,10 @@ Deno.test("generateCSS - lightDark colorScheme emits a color-scheme override rul ].join("\n"), ), css, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - lightDark names a paired variantNameOnly color by its variant", () => { +test("generateCSS - lightDark names a paired variantNameOnly color by its variant", () => { const variantNameOnly = { variantNameOnly: true }; const css = generateCSS( defineConfig({ @@ -133,10 +133,10 @@ Deno.test("generateCSS - lightDark names a paired variantNameOnly color by its v }), ); - assert(css.includes("--background: light-dark(white, black);"), css); + expect(css.includes("--background: light-dark(white, black);"), css).toBeTruthy(); }); -Deno.test("generateCSS - lightDark keeps unpaired themes with their selector", () => { +test("generateCSS - lightDark keeps unpaired themes with their selector", () => { const config = pairedConfig(); const theme = config.colors.theme as { value: Record }; theme.value.pink = { @@ -145,7 +145,7 @@ Deno.test("generateCSS - lightDark keeps unpaired themes with their selector", ( }; const css = generateCSS(config); - assert( + expect( css.endsWith( [ "/* Theme: pink */", @@ -156,12 +156,12 @@ Deno.test("generateCSS - lightDark keeps unpaired themes with their selector", ( ].join("\n"), ), css, - ); - assert(!css.includes("--theme-light-"), css); - assert(!css.includes("--theme-dark-"), css); + ).toBeTruthy(); + expect(!css.includes("--theme-light-"), css).toBeTruthy(); + expect(!css.includes("--theme-dark-"), css).toBeTruthy(); }); -Deno.test("generateCSS - a token path to a paired color resolves to the light-dark() token", () => { +test("generateCSS - a token path to a paired color resolves to the light-dark() token", () => { const config = pairedConfig(); config.primitives = { card: { @@ -175,13 +175,13 @@ Deno.test("generateCSS - a token path to a paired color resolves to the light-da }; const css = generateCSS(config); - assert( + expect( css.includes("--card-surface-background: var(--theme-background-primary);"), css, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - a paired mix references an earlier paired variant", () => { +test("generateCSS - a paired mix references an earlier paired variant", () => { const config = pairedConfig(); const theme = config.colors.theme as { value: Record }> }>; @@ -192,17 +192,17 @@ Deno.test("generateCSS - a paired mix references an earlier paired variant", () }; } - assert( + expect( generateCSS(config).includes( "--theme-background-hover: light-dark(color-mix(in oklch, var(--theme-background-primary), black 5%), color-mix(in oklch, var(--theme-background-primary), black 5%));", ), - ); + ).toBeTruthy(); }); -Deno.test("generateJSON - lightDark paired tokens sit at theme..", () => { +test("generateJSON - lightDark paired tokens sit at theme..", () => { const json = JSON.parse(generateJSON(pairedConfig())); - assertEquals(json.theme, { + expect(json.theme).toEqual({ background: { primary: { key: "--theme-background-primary", @@ -221,25 +221,25 @@ Deno.test("generateJSON - lightDark paired tokens sit at theme.. }, }, }); - assert( + expect( generateTS(pairedConfig()).includes('"key": "--theme-background-primary"'), "TS output carries the paired token", - ); + ).toBeTruthy(); }); -Deno.test("generateStyleDictionaryJSON - a paired token resolves both schemes and references both", () => { +test("generateStyleDictionaryJSON - a paired token resolves both schemes and references both", () => { const tokens = JSON.parse(generateStyleDictionaryJSON(pairedConfig())); const primary = tokens.theme.background.primary; - assertEquals(primary.value, "light-dark(oklch(100% 0 none), oklch(14.5% 0 none))"); - assertEquals(primary.type, "color"); - assertEquals(primary.attributes.referencePaths, [ + expect(primary.value).toEqual("light-dark(oklch(100% 0 none), oklch(14.5% 0 none))"); + expect(primary.type).toEqual("color"); + expect(primary.attributes.referencePaths).toEqual([ "palette.base.white", "palette.base.ink", ]); }); -Deno.test("generateCSS - lightDark rejects paired themes that declare different variants", () => { +test("generateCSS - lightDark rejects paired themes that declare different variants", () => { const config = pairedConfig(); const theme = config.colors.theme as { value: Record }> }>; @@ -255,7 +255,7 @@ Deno.test("generateCSS - lightDark rejects paired themes that declare different ); }); -Deno.test("generateCSS - lightDark rejects a selector or atRule on a paired theme", () => { +test("generateCSS - lightDark rejects a selector or atRule on a paired theme", () => { const config = pairedConfig(); const theme = config.colors.theme as { value: Record }; theme.value.dark.settings = { selector: ".dark" }; @@ -263,7 +263,7 @@ Deno.test("generateCSS - lightDark rejects a selector or atRule on a paired them throwsAt(config, '"theme.dark.settings"', '"selector"', "lightDark"); }); -Deno.test("generateCSS - lightDark rejects paired colors that disagree on variantNameOnly", () => { +test("generateCSS - lightDark rejects paired colors that disagree on variantNameOnly", () => { const config = pairedConfig(); const theme = config.colors.theme as { value: Record }>; @@ -273,7 +273,7 @@ Deno.test("generateCSS - lightDark rejects paired colors that disagree on varian throwsAt(config, '"theme.dark.text.settings.variantNameOnly"', '"theme.light.text"'); }); -Deno.test("generateCSS - lightDark rejects a paired color named like an unpaired theme", () => { +test("generateCSS - lightDark rejects a paired color named like an unpaired theme", () => { const config = pairedConfig(); const theme = config.colors.theme as { value: Record }; theme.value.text = { value: { accent: { value: { primary: "red" } } } }; @@ -281,7 +281,7 @@ Deno.test("generateCSS - lightDark rejects a paired color named like an unpaired throwsAt(config, '"theme.light.text"', 'the theme "text"'); }); -Deno.test("generateCSS - lightDark settings are validated with their configuration path", () => { +test("generateCSS - lightDark settings are validated with their configuration path", () => { throwsAt( withLightDark({ light: "light", dark: "night" }), '"theme.settings.lightDark.dark"', @@ -312,7 +312,7 @@ Deno.test("generateCSS - lightDark settings are validated with their configurati throwsAt(unknownKey, '"theme"', '"extra"'); }); -Deno.test("generateCSS - theme settings are rejected beside named themes", () => { +test("generateCSS - theme settings are rejected beside named themes", () => { throwsAt( { colors: { diff --git a/packages/cssforge/tests/load-config.test.ts b/packages/cssforge/tests/load-config.test.ts index e289f68..cfba77e 100644 --- a/packages/cssforge/tests/load-config.test.ts +++ b/packages/cssforge/tests/load-config.test.ts @@ -3,10 +3,10 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; +import { expect, test } from "vitest"; import type { LoadedConfig } from "../src/mod.ts"; import { loadConfig } from "../src/mod.ts"; import { childEnv } from "./helpers.ts"; -import { assert, assertEquals, Deno } from "./vitest-compat.ts"; const configSource = `import { size } from "./tokens.ts"; import { nestedSize } from "./nested.ts"; @@ -56,7 +56,7 @@ const runScenario = (dir: string): ScenarioResult => { .filter(Boolean) .pop(); - assertEquals(result.status, 0, `the loader scenario failed: ${result.stderr}`); + expect(result.status, `the loader scenario failed: ${result.stderr}`).toEqual(0); if (output === undefined) { throw new Error("the loader scenario printed no result"); @@ -98,12 +98,12 @@ const rejectWith = async ( throw new Error(message); }; -Deno.test("loadConfig - returns the default export and every local file the config loaded", async () => { +test("loadConfig - returns the default export and every local file the config loaded", async () => { await inConfigDir(configFiles, async (dir) => { const { first } = runScenario(dir); - assertEquals(first.config.spacing?.custom?.size?.value, { 2: "0.5rem", 4: "1rem" }); - assertEquals(first.dependencies.map((path) => path.replace(dir, "")).sort(), [ + expect(first.config.spacing?.custom?.size?.value).toEqual({ 2: "0.5rem", 4: "1rem" }); + expect(first.dependencies.map((path) => path.replace(dir, "")).sort()).toEqual([ "/cssforge.config.ts", "/nested.ts", "/tokens.ts", @@ -111,7 +111,7 @@ Deno.test("loadConfig - returns the default export and every local file the conf }); }); -Deno.test("loadConfig - loads a TypeScript config whatever the package type declares", async () => { +test("loadConfig - loads a TypeScript config whatever the package type declares", async () => { // An explicit `"type": "commonjs"` makes Node read `.ts` as CommonJS and turns // off module syntax detection, so `export default` would be a syntax error. const manifests = [ @@ -126,31 +126,28 @@ Deno.test("loadConfig - loads a TypeScript config whatever the package type decl async (dir) => { const { first } = runScenario(dir); - assertEquals( - first.config.spacing?.custom?.size?.value, + expect(first.config.spacing?.custom?.size?.value, `type field: ${label}`).toEqual( { 2: "0.5rem", 4: "1rem" }, - `type field: ${label}`, ); - assertEquals( + expect( first.dependencies.map((path) => path.replace(dir, "")).sort(), - ["/cssforge.config.ts", "/nested.ts", "/tokens.ts"], `type field: ${label}`, - ); + ).toEqual(["/cssforge.config.ts", "/nested.ts", "/tokens.ts"]); }, ); } }); -Deno.test("loadConfig - re-evaluates a token module the config imports on the next load", async () => { +test("loadConfig - re-evaluates a token module the config imports on the next load", async () => { await inConfigDir(configFiles, async (dir) => { const { first, second } = runScenario(dir); - assertEquals(first.config.spacing?.custom?.size?.value, { 2: "0.5rem", 4: "1rem" }); - assertEquals(second.config.spacing?.custom?.size?.value, { 2: "2rem", 4: "1rem" }); + expect(first.config.spacing?.custom?.size?.value).toEqual({ 2: "0.5rem", 4: "1rem" }); + expect(second.config.spacing?.custom?.size?.value).toEqual({ 2: "2rem", 4: "1rem" }); }); }); -Deno.test("loadConfig - reports the config path when the file does not exist", async () => { +test("loadConfig - reports the config path when the file does not exist", async () => { await inConfigDir({}, async (dir) => { const missing = join(dir, "cssforge.config.ts"); const error = await rejectWith( @@ -158,14 +155,14 @@ Deno.test("loadConfig - reports the config path when the file does not exist", a "loadConfig must reject for a missing config file", ); - assert( + expect( error.message.startsWith(`Could not load the CSS Forge config at ${missing}`), `error must name ${missing}: ${error.message}`, - ); + ).toBeTruthy(); }); }); -Deno.test("loadConfig - names the config path when the module throws while loading", async () => { +test("loadConfig - names the config path when the module throws while loading", async () => { await inConfigDir( { "cssforge.config.ts": `throw new Error("the token data is corrupt");\n` }, async (dir) => { @@ -175,21 +172,21 @@ Deno.test("loadConfig - names the config path when the module throws while loadi "loadConfig must reject when the config module throws", ); - assert( + expect( error.message.startsWith(`Could not load the CSS Forge config at ${path}`), `error must name ${path}: ${error.message}`, - ); - assert( + ).toBeTruthy(); + expect( (error.cause as Error | undefined)?.message.includes( "the token data is corrupt", ) === true, `the module error must be the cause: ${String(error.cause)}`, - ); + ).toBeTruthy(); }, ); }); -Deno.test("loadConfig - rejects a module whose default export is not a config object", async () => { +test("loadConfig - rejects a module whose default export is not a config object", async () => { await inConfigDir( { "cssforge.config.ts": `export const spacing = {};\n` }, async (dir) => { @@ -199,11 +196,14 @@ Deno.test("loadConfig - rejects a module whose default export is not a config ob "loadConfig must reject a config without a default export", ); - assert(error.message.includes(path), `error must name ${path}: ${error.message}`); - assert( + expect( + error.message.includes(path), + `error must name ${path}: ${error.message}`, + ).toBeTruthy(); + expect( error.message.includes("default export"), `error must describe the missing default export: ${error.message}`, - ); + ).toBeTruthy(); }, ); }); diff --git a/packages/cssforge/tests/motion.test.ts b/packages/cssforge/tests/motion.test.ts index efad22c..35e9172 100644 --- a/packages/cssforge/tests/motion.test.ts +++ b/packages/cssforge/tests/motion.test.ts @@ -1,3 +1,4 @@ +import { expect, test } from "vitest"; import { generateJSON, generateTS } from "../src/generator.ts"; import type { CSSForgeConfig, Diagnostic } from "../src/mod.ts"; import { @@ -6,7 +7,7 @@ import { getDiagnostics, processMotion, } from "../src/mod.ts"; -import { assert, assertEquals, assertThrows, Deno } from "./vitest-compat.ts"; +import { thrownError } from "./helpers.ts"; const motion = (config: CSSForgeConfig["motion"]): Partial => ({ motion: config, @@ -22,13 +23,16 @@ const easing = (value: string) => motion({ easing: { ui: { value: { enter: value const codes = (diagnostics: Diagnostic[]) => diagnostics.map(({ code }) => code); const assertRejected = (config: unknown, parts: string[]) => { - const error = assertThrows(() => generateCSS(config as Partial)); + const error = thrownError(() => generateCSS(config as Partial)); for (const part of parts) { - assert(error.message.includes(part), `Expected "${part}" in: ${error.message}`); + expect( + error.message.includes(part), + `Expected "${part}" in: ${error.message}`, + ).toBeTruthy(); } }; -Deno.test("motion - emits duration and easing tokens per group", () => { +test("motion - emits duration and easing tokens per group", () => { const css = generateCSS( motion({ duration: { @@ -41,8 +45,7 @@ Deno.test("motion - emits duration and easing tokens per group", () => { }), ); - assertEquals( - css, + expect(css).toEqual( [ "/*____ CSSForge ____*/", ":root {", @@ -57,13 +60,13 @@ Deno.test("motion - emits duration and easing tokens per group", () => { ); }); -Deno.test("motion - processMotion resolves motion... paths", () => { +test("motion - processMotion resolves motion... paths", () => { const { resolveMap } = processMotion({ duration: { ui: { value: { press: "120ms" } } }, easing: { ui: { value: { out: "ease-out" } } }, }); - assertEquals(Object.fromEntries(resolveMap), { + expect(Object.fromEntries(resolveMap)).toEqual({ "motion.duration.ui.press": { key: "--motion-duration-ui-press", value: "120ms", @@ -83,7 +86,7 @@ Deno.test("motion - processMotion resolves motion... paths", }); }); -Deno.test("motion - primitives reference motion tokens through variables", () => { +test("motion - primitives reference motion tokens through variables", () => { const css = generateCSS({ ...motion({ duration: { ui: { value: { press: "120ms" } } }, @@ -101,15 +104,15 @@ Deno.test("motion - primitives reference motion tokens through variables", () => }, }); - assert( + expect( css.includes( "--button-default-transition: transform var(--motion-duration-ui-press) var(--motion-easing-ui-out);", ), css, - ); + ).toBeTruthy(); }); -Deno.test("motion - a reference may keep the value segment, as spacing allows", () => { +test("motion - a reference may keep the value segment, as spacing allows", () => { const css = generateCSS({ ...motion({ duration: { ui: { value: { press: "120ms" } } } }), primitives: { @@ -123,14 +126,14 @@ Deno.test("motion - a reference may keep the value segment, as spacing allows", }, }, }); - assert( + expect( css.includes("--button-default-duration: var(--motion-duration-ui-press);"), css, - ); + ).toBeTruthy(); }); -Deno.test("motion - an unknown motion reference names the path", () => { - const error = assertThrows(() => +test("motion - an unknown motion reference names the path", () => { + const error = thrownError(() => generateCSS({ ...motion({ duration: { ui: { value: { press: "120ms" } } } }), primitives: { @@ -145,34 +148,36 @@ Deno.test("motion - an unknown motion reference names the path", () => { }, }), ); - assert(error.message.includes("motion.duration.ui.missing"), error.message); + expect( + error.message.includes("motion.duration.ui.missing"), + error.message, + ).toBeTruthy(); }); -Deno.test("motion - JSON, TypeScript and Style Dictionary outputs carry the tokens", () => { +test("motion - JSON, TypeScript and Style Dictionary outputs carry the tokens", () => { const config = motion({ duration: { ui: { value: { press: "120ms" } } }, easing: { ui: { value: { out: "ease-out" } } }, }); const json = JSON.parse(generateJSON(config)); - assertEquals(json.motion.duration.ui.press, { + expect(json.motion.duration.ui.press).toEqual({ key: "--motion-duration-ui-press", value: "120ms", variable: "--motion-duration-ui-press: 120ms;", }); - assert(generateTS(config).includes('"key": "--motion-easing-ui-out"')); + expect(generateTS(config).includes('"key": "--motion-easing-ui-out"')).toBeTruthy(); const tokens = JSON.parse(generateStyleDictionaryJSON(config)); - assertEquals(tokens.motion.duration.ui.press.value, "120ms"); - assertEquals(tokens.motion.duration.ui.press.type, "duration"); - assertEquals(tokens.motion.easing.ui.out.type, "easing"); - assertEquals( - tokens.motion.easing.ui.out.attributes.cssVariable, + expect(tokens.motion.duration.ui.press.value).toEqual("120ms"); + expect(tokens.motion.duration.ui.press.type).toEqual("duration"); + expect(tokens.motion.easing.ui.out.type).toEqual("easing"); + expect(tokens.motion.easing.ui.out.attributes.cssVariable).toEqual( "--motion-easing-ui-out", ); }); -Deno.test("motion - a token colliding with a primitive key is rejected", () => { +test("motion - a token colliding with a primitive key is rejected", () => { assertRejected( { ...motion({ duration: { ui: { value: { press: "120ms" } } } }), @@ -186,13 +191,13 @@ Deno.test("motion - a token colliding with a primitive key is rejected", () => { ); }); -Deno.test("motion - accepts durations in ms or s, including zero", () => { +test("motion - accepts durations in ms or s, including zero", () => { for (const value of ["0ms", "0s", "120ms", "0.2s", ".15s", "1.5ms"]) { - assertEquals(getDiagnostics(duration(value)), [], value); + expect(getDiagnostics(duration(value)), value).toEqual([]); } }); -Deno.test("motion - rejects a duration that is not a non-negative ms or s value", () => { +test("motion - rejects a duration that is not a non-negative ms or s value", () => { for (const value of [ "120", "0", @@ -216,7 +221,7 @@ Deno.test("motion - rejects a duration that is not a non-negative ms or s value" ]); }); -Deno.test("motion - accepts CSS easing functions", () => { +test("motion - accepts CSS easing functions", () => { for (const value of [ "linear", "ease", @@ -237,11 +242,11 @@ Deno.test("motion - accepts CSS easing functions", () => { "linear(0, 0.5 25% 75%, 1)", "linear(0, 25% 75% 0.5, 1)", ]) { - assertEquals(codes(getDiagnostics(easing(value))), [], value); + expect(codes(getDiagnostics(easing(value))), value).toEqual([]); } }); -Deno.test("motion - rejects values that are not CSS easing functions", () => { +test("motion - rejects values that are not CSS easing functions", () => { for (const value of [ "", "ease-inn", @@ -268,7 +273,7 @@ Deno.test("motion - rejects values that are not CSS easing functions", () => { } }); -Deno.test("motion - rejects malformed groups and settings", () => { +test("motion - rejects malformed groups and settings", () => { assertRejected(motion({ durations: {} } as never), ['"motion"', '"durations"']); assertRejected(motion({ duration: { ui: { press: "120ms" } } } as never), [ '"motion.duration.ui"', @@ -293,37 +298,37 @@ Deno.test("motion - rejects malformed groups and settings", () => { ]); }); -Deno.test("motion - a UI duration over 300ms warns, 300ms does not", () => { - assertEquals(getDiagnostics(duration("300ms")), []); - assertEquals(getDiagnostics(duration("0.3s")), []); +test("motion - a UI duration over 300ms warns, 300ms does not", () => { + expect(getDiagnostics(duration("300ms"))).toEqual([]); + expect(getDiagnostics(duration("0.3s"))).toEqual([]); const [diagnostic, ...rest] = getDiagnostics(duration("0.301s")); - assertEquals(rest, []); - assertEquals(diagnostic?.code, "motion-long-duration"); - assertEquals(diagnostic?.severity, "warning"); - assertEquals(diagnostic?.path, "motion.duration.ui.press"); + expect(rest).toEqual([]); + expect(diagnostic?.code).toEqual("motion-long-duration"); + expect(diagnostic?.severity).toEqual("warning"); + expect(diagnostic?.path).toEqual("motion.duration.ui.press"); for (const part of ["motion.duration.ui.press", "301ms", "300ms", "settings.long"]) { - assert( + expect( diagnostic?.message.includes(part), `Expected "${part}" in: ${diagnostic?.message}`, - ); + ).toBeTruthy(); } }); -Deno.test("motion - a long group allows up to 500ms and warns above it", () => { - assertEquals(getDiagnostics(duration("400ms", { long: true })), []); - assertEquals(getDiagnostics(duration("500ms", { long: true })), []); +test("motion - a long group allows up to 500ms and warns above it", () => { + expect(getDiagnostics(duration("400ms", { long: true }))).toEqual([]); + expect(getDiagnostics(duration("500ms", { long: true }))).toEqual([]); const [diagnostic] = getDiagnostics(duration("501ms", { long: true })); - assertEquals(diagnostic?.code, "motion-long-duration"); - assert(diagnostic?.message.includes("500ms"), diagnostic?.message); + expect(diagnostic?.code).toEqual("motion-long-duration"); + expect(diagnostic?.message.includes("500ms"), diagnostic?.message).toBeTruthy(); - assertEquals(codes(getDiagnostics(duration("400ms", { long: false }))), [ + expect(codes(getDiagnostics(duration("400ms", { long: false })))).toEqual([ "motion-long-duration", ]); }); -Deno.test("motion - ease-in and ease-in shaped curves warn", () => { +test("motion - ease-in and ease-in shaped curves warn", () => { for (const value of [ "ease-in", "cubic-bezier(0.42, 0, 1, 1)", @@ -332,16 +337,16 @@ Deno.test("motion - ease-in and ease-in shaped curves warn", () => { "cubic-bezier(0.6, -0.28, 0.735, 0.045)", ]) { const diagnostics = getDiagnostics(easing(value)); - assertEquals(codes(diagnostics), ["motion-ease-in"], value); - assertEquals(diagnostics[0]?.path, "motion.easing.ui.enter"); - assert( + expect(codes(diagnostics), value).toEqual(["motion-ease-in"]); + expect(diagnostics[0]?.path).toEqual("motion.easing.ui.enter"); + expect( diagnostics[0]?.message.includes("motion.easing.ui.enter"), diagnostics[0]?.message, - ); + ).toBeTruthy(); } }); -Deno.test("motion - ease-out, ease-in-out and linear curves do not warn", () => { +test("motion - ease-out, ease-in-out and linear curves do not warn", () => { for (const value of [ "ease", "ease-out", @@ -355,6 +360,6 @@ Deno.test("motion - ease-out, ease-in-out and linear curves do not warn", () => "steps(4)", "linear(0, 0.1 50%, 1)", ]) { - assertEquals(getDiagnostics(easing(value)), [], value); + expect(getDiagnostics(easing(value)), value).toEqual([]); } }); diff --git a/packages/cssforge/tests/primitive.test.ts b/packages/cssforge/tests/primitive.test.ts index 0695103..6fb8fa7 100644 --- a/packages/cssforge/tests/primitive.test.ts +++ b/packages/cssforge/tests/primitive.test.ts @@ -1,8 +1,8 @@ +import { expect, test } from "vitest"; import { defineConfig } from "../src/mod.ts"; import { processPrimitives } from "../src/modules/primitive.ts"; -import { assertEquals, assertSnapshot, Deno } from "./vitest-compat.ts"; -Deno.test("processPrimitives - processes button with variables", async (t) => { +test("processPrimitives - processes button with variables", () => { const config = defineConfig({ typography: { fluid: { @@ -64,12 +64,12 @@ Deno.test("processPrimitives - processes button with variables", async (t) => { "--button-small-padding: var(--spacing-size-2) var(--spacing-size-3);", ].join("\n"); - assertEquals(result.css.root, expected); - await assertSnapshot(t, result.css); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(result.css.root).toEqual(expected); + expect(result.css).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processPrimitives - processes buttons with settings", async (t) => { +test("processPrimitives - processes buttons with settings", () => { const config = defineConfig({ primitives: { button: { @@ -106,12 +106,12 @@ Deno.test("processPrimitives - processes buttons with settings", async (t) => { "--button-big-radius: 1rem;", ].join("\n"); - assertEquals(result.css.root, expected); - await assertSnapshot(t, result.css); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(result.css.root).toEqual(expected); + expect(result.css).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processPrimitives - references colors, gradients, and themes", async (t) => { +test("processPrimitives - references colors, gradients, and themes", () => { const config = defineConfig({ colors: { palette: { @@ -181,12 +181,12 @@ Deno.test("processPrimitives - references colors, gradients, and themes", async "--card-default-border-color: var(--palette-coral-50);", ].join("\n"); - assertEquals(result.css.root, expected); - await assertSnapshot(t, result.css); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(result.css.root).toEqual(expected); + expect(result.css).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processPrimitives - uses fluid spacing references", async (t) => { +test("processPrimitives - uses fluid spacing references", () => { const config = defineConfig({ spacing: { fluid: { @@ -225,12 +225,12 @@ Deno.test("processPrimitives - uses fluid spacing references", async (t) => { "/* box */", "--box-default-padding: var(--spacing_fluid-gap-gs-s) var(--spacing_fluid-gap-gs-m);", ].join("\n"); - assertEquals(primitives.css.root, expected); - await assertSnapshot(t, primitives.css); - await assertSnapshot(t, Array.from(primitives.resolveMap.entries())); + expect(primitives.css.root).toEqual(expected); + expect(primitives.css).toMatchSnapshot(); + expect(Array.from(primitives.resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processPrimitives - converts multiple px values with pxToRem enabled", async () => { +test("processPrimitives - converts multiple px values with pxToRem enabled", () => { const config = defineConfig({ primitives: { box: { @@ -254,10 +254,10 @@ Deno.test("processPrimitives - converts multiple px values with pxToRem enabled" "--box-padded-margin: 0 0 0.25rem;", ].join("\n"); - assertEquals(result.css.root, expected); + expect(result.css.root).toEqual(expected); }); -Deno.test("processPrimitives - preserves px in functions with pxToRem enabled", async () => { +test("processPrimitives - preserves px in functions with pxToRem enabled", () => { const config = defineConfig({ primitives: { btn: { @@ -281,10 +281,10 @@ Deno.test("processPrimitives - preserves px in functions with pxToRem enabled", "--btn-rounded-border: 0.0625rem solid var(--color, #000);", ].join("\n"); - assertEquals(result.css.root, expected); + expect(result.css.root).toEqual(expected); }); -Deno.test("processPrimitives - resolves hyphenated aliases with fallbacks end to end", async (t) => { +test("processPrimitives - resolves hyphenated aliases with fallbacks end to end", () => { const config = defineConfig({ colors: { palette: { @@ -338,7 +338,7 @@ Deno.test("processPrimitives - resolves hyphenated aliases with fallbacks end to "--panel-default-border-color: var(--brand-color, #fff);", ].join("\n"); - assertEquals(result.css.root, expected); - await assertSnapshot(t, result.css); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(result.css.root).toEqual(expected); + expect(result.css).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); diff --git a/packages/cssforge/tests/quick-start.test.ts b/packages/cssforge/tests/quick-start.test.ts index 85f875f..0f9f57f 100644 --- a/packages/cssforge/tests/quick-start.test.ts +++ b/packages/cssforge/tests/quick-start.test.ts @@ -5,8 +5,8 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import type { ArgsDef } from "citty"; +import { expect, test } from "vitest"; import mainCommand from "../src/cli.ts"; -import { assert, assertEquals, Deno } from "./vitest-compat.ts"; // The fixtures are excerpts of README.md that must match it byte for byte, so // they keep a `.txt` suffix: the TypeScript one imports the generated @@ -24,7 +24,7 @@ const allowedFenceLanguages = ["bash", ...scannedFenceLanguages]; const readmeSection = (readme: string, title: string): string => { const heading = `## ${title}`; const start = readme.indexOf(heading); - assert(start >= 0, `README.md must contain a '${heading}' section`); + expect(start >= 0, `README.md must contain a '${heading}' section`).toBeTruthy(); const end = readme.indexOf("\n## ", start + heading.length); return readme.slice(start, end === -1 ? readme.length : end); @@ -45,7 +45,9 @@ const documentedOutputPaths = (): { css: string; ts: string } => { const args = (mainCommand.args ?? {}) as ArgsDef; const defaultOf = (name: string): string => { const value = args[name]?.default; - assertEquals(typeof value, "string", `the CLI must declare a default for --${name}`); + expect(typeof value, `the CLI must declare a default for --${name}`).toEqual( + "string", + ); return value as string; }; @@ -53,7 +55,7 @@ const documentedOutputPaths = (): { css: string; ts: string } => { return { css: defaultOf("css"), ts: defaultOf("ts") }; }; -Deno.test("quick start - documented usage matches generated tokens and output paths", async () => { +test("quick start - documented usage matches generated tokens and output paths", async () => { const readme = await readFile( fileURLToPath(new URL("../../../README.md", import.meta.url)), "utf8", @@ -63,10 +65,10 @@ Deno.test("quick start - documented usage matches generated tokens and output pa const tsExample = await readFixture("consumer.ts.txt"); const { css: cssPath, ts: tsPath } = documentedOutputPaths(); - assertEquals(cssPath, "./.cssforge/output.css"); - assertEquals(tsPath, "./.cssforge/output.ts"); - assertEquals(cssExample.match(/@import\s+"([^"]+)"/)?.[1], cssPath); - assertEquals(tsExample.match(/from\s+"([^"]+)"/)?.[1], tsPath); + expect(cssPath).toEqual("./.cssforge/output.css"); + expect(tsPath).toEqual("./.cssforge/output.ts"); + expect(cssExample.match(/@import\s+"([^"]+)"/)?.[1]).toEqual(cssPath); + expect(tsExample.match(/from\s+"([^"]+)"/)?.[1]).toEqual(tsPath); const projectDir = await mkdtemp(join(tmpdir(), "cssforge-quick-start-")); @@ -94,12 +96,12 @@ Deno.test("quick start - documented usage matches generated tokens and output pa [fileURLToPath(new URL("../src/cli.ts", import.meta.url))], { cwd: projectDir, encoding: "utf8", env: childEnv }, ); - assertEquals(generate.status, 0, generate.stderr); + expect(generate.status, generate.stderr).toEqual(0); const cssOutput = resolve(projectDir, cssPath); const tsOutput = resolve(projectDir, tsPath); - assertEquals(existsSync(cssOutput), true); - assertEquals(existsSync(tsOutput), true); + expect(existsSync(cssOutput)).toEqual(true); + expect(existsSync(tsOutput)).toEqual(true); const css = await readFile(cssOutput, "utf8"); const declared = new Set( @@ -110,11 +112,11 @@ Deno.test("quick start - documented usage matches generated tokens and output pa const consumed = [...cssExample.matchAll(/var\((--[\w-]+)\)/g)].map( (match) => match[1], ); - assert(consumed.length > 0, "the CSS example must consume custom properties"); - assertEquals( - consumed.filter((name) => !declared.has(name)), - [], - ); + expect( + consumed.length > 0, + "the CSS example must consume custom properties", + ).toBeTruthy(); + expect(consumed.filter((name) => !declared.has(name))).toEqual([]); // No token name in the Quick Start may be missing from the generated // declarations, so the documented drift cannot return silently. @@ -122,35 +124,34 @@ Deno.test("quick start - documented usage matches generated tokens and output pa const fences = [...quickStart.matchAll(/^```([^\n]*)\n([\s\S]*?)^```$/gm)].map( (match) => ({ language: match[1], body: match[2] }), ); - assertEquals( + expect( fences.filter((fence) => !allowedFenceLanguages.includes(fence.language)), - [], "the Quick Start must only fence code in languages this scan understands", - ); + ).toEqual([]); const referenced = fences .filter((fence) => scannedFenceLanguages.includes(fence.language)) .flatMap((fence) => [...fence.body.matchAll(/--[a-zA-Z][\w-]*/g)].map((match) => match[0]), ); - assert(referenced.length > 0, "the Quick Start must reference generated tokens"); - assertEquals( - [...new Set(referenced)].filter((name) => !declared.has(name)), - [], - ); + expect( + referenced.length > 0, + "the Quick Start must reference generated tokens", + ).toBeTruthy(); + expect([...new Set(referenced)].filter((name) => !declared.has(name))).toEqual([]); // README.md must contain the fixtures exactly. - assert( + expect( readme.includes(configFixture.trimEnd()), "README.md does not contain the Quick Start configuration fixture", - ); - assert( + ).toBeTruthy(); + expect( readme.includes(cssExample.trimEnd()), "README.md does not contain the CSS usage fixture", - ); - assert( + ).toBeTruthy(); + expect( readme.includes(tsExample.trimEnd()), "README.md does not contain the TypeScript usage fixture", - ); + ).toBeTruthy(); // The documented property chain must type-check and resolve. const usagePath = join(projectDir, "consumer.ts"); @@ -184,13 +185,16 @@ Deno.test("quick start - documented usage matches generated tokens and output pa ".bin", executableName("tsc"), ); - assert(existsSync(tsc), "typescript is required to type-check the example"); + expect( + existsSync(tsc), + "typescript is required to type-check the example", + ).toBeTruthy(); const typecheck = spawnSync(tsc, ["--project", join(projectDir, "tsconfig.json")], { cwd: projectDir, encoding: "utf8", env: childEnv, }); - assertEquals(typecheck.status, 0, `${typecheck.stdout}\n${typecheck.stderr}`); + expect(typecheck.status, `${typecheck.stdout}\n${typecheck.stderr}`).toEqual(0); const runExample = spawnSync( process.execPath, @@ -203,14 +207,14 @@ Deno.test("quick start - documented usage matches generated tokens and output pa ], { cwd: projectDir, encoding: "utf8", env: childEnv }, ); - assertEquals(runExample.status, 0, runExample.stderr); + expect(runExample.status, runExample.stderr).toEqual(0); const token = JSON.parse(runExample.stdout) as { key: string; value: string; variable: string; }; - assertEquals(token, { + expect(token).toEqual({ key: "--spacing-size-2", value: "0.5rem", variable: "--spacing-size-2: 0.5rem;", diff --git a/packages/cssforge/tests/settings-validation.test.ts b/packages/cssforge/tests/settings-validation.test.ts index c88014a..dee619d 100644 --- a/packages/cssforge/tests/settings-validation.test.ts +++ b/packages/cssforge/tests/settings-validation.test.ts @@ -1,6 +1,7 @@ +import { expect, test } from "vitest"; import type { CSSForgeConfig } from "../src/mod.ts"; import { defineConfig, generateCSS } from "../src/mod.ts"; -import { assert, assertThrows, Deno } from "./vitest-compat.ts"; +import { thrownError } from "./helpers.ts"; /** Settings are read from a JavaScript object at runtime, so every module validates them. */ @@ -17,47 +18,47 @@ const fluidValue = { const palette = { value: { coral: { 100: { hex: "#FF7F50" } } } }; -Deno.test("generateCSS - rejects a spacing settings value that is not an object", () => { +test("generateCSS - rejects a spacing settings value that is not an object", () => { const config = { spacing: { custom: { size: { value: { 2: "8px" }, settings: "rem" } } }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"spacing.custom.size.settings"'), `Expected the error to name the scale. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an unknown spacing setting", () => { +test("generateCSS - rejects an unknown spacing setting", () => { const config = { spacing: { custom: { size: { value: { 2: "8px" }, settings: { remm: 16 } } } }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"remm"') && error.message.includes('"pxToRem"'), `Expected the error to name the key and the accepted ones. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an unknown typography setting", () => { +test("generateCSS - rejects an unknown typography setting", () => { const config = { typography: { fluid: { base: { value: fluidValue, settings: { customLabl: {} } } } }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"typography_fluid.base.settings"') && error.message.includes('"customLabel"'), `Expected the error to name the path and the accepted key. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects settings a primitive group does not read", () => { +test("generateCSS - rejects settings a primitive group does not read", () => { const config = { primitives: { button: { @@ -67,16 +68,16 @@ Deno.test("generateCSS - rejects settings a primitive group does not read", () = }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"primitives.button.settings"') && error.message.includes("reads no settings"), `Expected the error to name the group and the level. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - rejects an unknown primitive variant setting", () => { +test("generateCSS - rejects an unknown primitive variant setting", () => { const config = { primitives: { button: { @@ -90,16 +91,16 @@ Deno.test("generateCSS - rejects an unknown primitive variant setting", () => { }, } as unknown as CSSForgeConfig; - const error = assertThrows(() => generateCSS(config)); + const error = thrownError(() => generateCSS(config)); - assert( + expect( error.message.includes('"primitives.button.default.settings"') && error.message.includes('"foo"'), `Expected the error to name the variant and the key. Received: ${error.message}`, - ); + ).toBeTruthy(); }); -Deno.test("generateCSS - a valid settings object still generates", () => { +test("generateCSS - a valid settings object still generates", () => { const config = defineConfig({ colors: { palette }, spacing: { @@ -117,7 +118,7 @@ Deno.test("generateCSS - a valid settings object still generates", () => { const css = generateCSS(config); - assert(css.includes("--spacing-size-2: 0.5rem;"), css); - assert(css.includes("--typography_fluid-base-mid:"), css); - assert(css.includes("--button-default-padding: 8px;"), css); + expect(css.includes("--spacing-size-2: 0.5rem;"), css).toBeTruthy(); + expect(css.includes("--typography_fluid-base-mid:"), css).toBeTruthy(); + expect(css.includes("--button-default-padding: 8px;"), css).toBeTruthy(); }); diff --git a/packages/cssforge/tests/spacing.test.ts b/packages/cssforge/tests/spacing.test.ts index 2f929dd..cc7f6bd 100644 --- a/packages/cssforge/tests/spacing.test.ts +++ b/packages/cssforge/tests/spacing.test.ts @@ -1,8 +1,8 @@ +import { expect, test } from "vitest"; import { defineConfig } from "../src/config.ts"; import { processSpacing } from "../src/modules/spacing.ts"; -import { assertEquals, assertSnapshot, Deno } from "./vitest-compat.ts"; -Deno.test("processSpacing - generates correct spacing scale", async (t) => { +test("processSpacing - generates correct spacing scale", () => { const config = defineConfig({ spacing: { custom: { @@ -20,20 +20,20 @@ Deno.test("processSpacing - generates correct spacing scale", async (t) => { "--spacing-size-3: 0.75rem;", "--spacing-size-s: 1rem;", ].join("\n"); - assertEquals(result.css.root, expected); + expect(result.css.root).toEqual(expected); - assertEquals(Array.from(result.resolveMap.keys()), [ + expect(Array.from(result.resolveMap.keys())).toEqual([ "spacing.custom.size.1", "spacing.custom.size.2", "spacing.custom.size.3", "spacing.custom.size.s", ]); - await assertSnapshot(t, result.css.root); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(result.css.root).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); // Test with string keys -Deno.test("processSpacing - handles settings", async (t) => { +test("processSpacing - handles settings", () => { const config = defineConfig({ spacing: { custom: { @@ -57,19 +57,19 @@ Deno.test("processSpacing - handles settings", async (t) => { "--spacing-scale-lg: 0.75rem;", ].join("\n"); - assertEquals(result.css.root, expected); + expect(result.css.root).toEqual(expected); - assertEquals(Array.from(result.resolveMap.keys()), [ + expect(Array.from(result.resolveMap.keys())).toEqual([ "spacing.custom.size.1", "spacing.custom.size.2", "spacing.custom.scale.md", "spacing.custom.scale.lg", ]); - await assertSnapshot(t, result.css.root); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(result.css.root).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processSpacing - generates fluid spacing (prefix)", async (t) => { +test("processSpacing - generates fluid spacing (prefix)", () => { const config = defineConfig({ spacing: { fluid: { @@ -100,9 +100,9 @@ Deno.test("processSpacing - generates fluid spacing (prefix)", async (t) => { "--spacing_fluid-base-foo-s-m: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);", "--spacing_fluid-base-foo-m-l: clamp(0.25rem, -0.6667rem + 4.5833vw, 3rem);", ].join("\n"); - assertEquals(css.root, expected); + expect(css.root).toEqual(expected); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "spacing_fluid.base@xs", "spacing_fluid.base@s", "spacing_fluid.base@m", @@ -112,11 +112,11 @@ Deno.test("processSpacing - generates fluid spacing (prefix)", async (t) => { "spacing_fluid.base@s-m", "spacing_fluid.base@m-l", ]); - await assertSnapshot(t, css.root); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(css.root).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processSpacing - fluid without prefix falls back to scale name", async (t) => { +test("processSpacing - fluid without prefix falls back to scale name", () => { const config = defineConfig({ spacing: { fluid: { @@ -141,20 +141,20 @@ Deno.test("processSpacing - fluid without prefix falls back to scale name", asyn "--spacing_fluid-rhythm-xs-s: clamp(0rem, -0.4167rem + 2.0833vw, 1.25rem);", "--spacing_fluid-rhythm-s-m: clamp(0.125rem, -0.25rem + 1.875vw, 1.25rem);", ].join("\n"); - assertEquals(css.root, expected); + expect(css.root).toEqual(expected); - assertEquals(Array.from(resolveMap.keys()), [ + expect(Array.from(resolveMap.keys())).toEqual([ "spacing_fluid.rhythm@xs", "spacing_fluid.rhythm@s", "spacing_fluid.rhythm@m", "spacing_fluid.rhythm@xs-s", "spacing_fluid.rhythm@s-m", ]); - await assertSnapshot(t, css.root); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(css.root).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processSpacing - combines fluid and custom spacing", async (t) => { +test("processSpacing - combines fluid and custom spacing", () => { const config = defineConfig({ spacing: { fluid: { @@ -192,8 +192,8 @@ Deno.test("processSpacing - combines fluid and custom spacing", async (t) => { "--spacing-gap-1: 4px;", "--spacing-gap-2: 8px;", ].join("\n"); - assertEquals(css.root, expected); - assertEquals(Array.from(resolveMap.keys()), [ + expect(css.root).toEqual(expected); + expect(Array.from(resolveMap.keys())).toEqual([ "spacing_fluid.base@xs", "spacing_fluid.base@s", "spacing_fluid.base@m", @@ -202,11 +202,11 @@ Deno.test("processSpacing - combines fluid and custom spacing", async (t) => { "spacing.custom.gap.1", "spacing.custom.gap.2", ]); - await assertSnapshot(t, css.root); - await assertSnapshot(t, Array.from(resolveMap.entries())); + expect(css.root).toMatchSnapshot(); + expect(Array.from(resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processSpacing - handles multiple px values in a single spacing value", async () => { +test("processSpacing - handles multiple px values in a single spacing value", () => { const config = defineConfig({ spacing: { custom: { @@ -234,10 +234,10 @@ Deno.test("processSpacing - handles multiple px values in a single spacing value "--spacing-pad-exponent: 6.25rem;", "--spacing-pad-shadow: 0 0 0.25rem red, 0 0 0.5rem blue;", ].join("\n"); - assertEquals(result.css.root, expected); + expect(result.css.root).toEqual(expected); }); -Deno.test("processSpacing - preserves px values inside calc and var functions", async () => { +test("processSpacing - preserves px values inside calc and var functions", () => { const config = defineConfig({ spacing: { custom: { @@ -259,10 +259,10 @@ Deno.test("processSpacing - preserves px values inside calc and var functions", "--spacing-size-var_px: var(--x, 4px);", "--spacing-size-nested: calc(infinity * 1px);", ].join("\n"); - assertEquals(result.css.root, expected); + expect(result.css.root).toEqual(expected); }); -Deno.test("processSpacing - handles mixed px and non-px values", async () => { +test("processSpacing - handles mixed px and non-px values", () => { const config = defineConfig({ spacing: { custom: { @@ -278,5 +278,5 @@ Deno.test("processSpacing - handles mixed px and non-px values", async () => { const result = processSpacing(config.spacing); const expected = "--spacing-border-shorthand: 0.0625rem solid red;"; - assertEquals(result.css.root, expected); + expect(result.css.root).toEqual(expected); }); diff --git a/packages/cssforge/tests/typography.test.ts b/packages/cssforge/tests/typography.test.ts index cce0157..a783eb8 100644 --- a/packages/cssforge/tests/typography.test.ts +++ b/packages/cssforge/tests/typography.test.ts @@ -1,6 +1,6 @@ +import { expect, test } from "vitest"; import { defineConfig, processTypography } from "../src/mod.ts"; import { getLines } from "./helpers.ts"; -import { assertEquals, assertSnapshot, Deno } from "./vitest-compat.ts"; /** The paths of a fluid scale's tokens: its clamp() steps, then its pow tokens. */ const fluidPaths = (scale: string, labels: string[]) => [ @@ -19,7 +19,7 @@ const fluidPaths = (scale: string, labels: string[]) => [ ...labels.map((label) => `typography_fluid.${scale}.pow@${label}`), ]; -Deno.test("processTypography - generates correct CSS variables", async (t) => { +test("processTypography - generates correct CSS variables", () => { const config = defineConfig({ typography: { fluid: { @@ -42,29 +42,30 @@ Deno.test("processTypography - generates correct CSS variables", async (t) => { const expectedSizes = ["4xl", "3xl", "2xl", "xl", "l", "m", "s", "xs", "2xs"]; const result = processTypography(config.typography); const lines = getLines(result.css.root); - assertEquals(lines.length, 2 * expectedSizes.length + 9); + expect(lines.length).toEqual(2 * expectedSizes.length + 9); // Test a specific value for precision const xlLine = lines.find((line) => line.includes("--typography_fluid-arial-xl:")); - assertEquals( - xlLine, + expect(xlLine).toEqual( "--typography_fluid-arial-xl: clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem);", ); - assertEquals(Array.from(result.resolveMap.keys()), fluidPaths("arial", expectedSizes)); + expect(Array.from(result.resolveMap.keys())).toEqual( + fluidPaths("arial", expectedSizes), + ); // Test that we have all the expected size variables expectedSizes.forEach((size) => { const hasSize = lines.some((line) => line.includes(`--typography_fluid-arial-${size}:`), ); - assertEquals(hasSize, true, `Missing size ${size}`); + expect(hasSize, `Missing size ${size}`).toEqual(true); }); - await assertSnapshot(t, result.css); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(result.css).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("typography - can handle custom labels and prefixes", async (t) => { +test("typography - can handle custom labels and prefixes", () => { const config = defineConfig({ typography: { fluid: { @@ -102,17 +103,15 @@ Deno.test("typography - can handle custom labels and prefixes", async (t) => { const expectedSizes = ["xs", "s", "m", "l", "xl", "xxl", "h4", "h3", "h2", "h1"]; const result = processTypography(config.typography); const lines = getLines(result.css.root); - assertEquals(lines.length, 2 * expectedSizes.length + 9); + expect(lines.length).toEqual(2 * expectedSizes.length + 9); // Test a specific value for precision const xlLine = lines.find((line) => line.includes("--typography_fluid-arial-text-xl:")); - assertEquals( - xlLine, + expect(xlLine).toEqual( "--typography_fluid-arial-text-xl: clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem);", ); - assertEquals( - Array.from(result.resolveMap.keys()), + expect(Array.from(result.resolveMap.keys())).toEqual( fluidPaths("arial", expectedSizes.toReversed()), ); @@ -121,13 +120,13 @@ Deno.test("typography - can handle custom labels and prefixes", async (t) => { const hasSize = lines.some((line) => line.includes(`--typography_fluid-arial-text-${size}:`), ); - assertEquals(hasSize, true, `Missing size ${size}`); + expect(hasSize, `Missing size ${size}`).toEqual(true); }); - await assertSnapshot(t, result.css); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(result.css).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); -Deno.test("processTypography - can process weights", async (t) => { +test("processTypography - can process weights", () => { const positiveSteps = 3; const negativeSteps = 3; const config = defineConfig({ @@ -155,9 +154,9 @@ Deno.test("processTypography - can process weights", async (t) => { const result = processTypography(config.typography); const lines = getLines(result.css.root); // Each step twice, the 9 pow inputs and helpers, and the weight. - assertEquals(lines.length, 2 * (positiveSteps + negativeSteps + 1) + 9 + 1); + expect(lines.length).toEqual(2 * (positiveSteps + negativeSteps + 1) + 9 + 1); - assertEquals(Array.from(result.resolveMap.keys()), [ + expect(Array.from(result.resolveMap.keys())).toEqual([ ...fluidPaths("arial", ["2xl", "xl", "l", "m", "s", "xs", "2xs"]), "typography.weight.arial.regular", ]); @@ -165,7 +164,7 @@ Deno.test("processTypography - can process weights", async (t) => { const xlLine = lines.find((line) => line.includes("--typography-weight-arial-regular:"), ); - assertEquals(xlLine, "--typography-weight-arial-regular: 500;"); - await assertSnapshot(t, result.css); - await assertSnapshot(t, Array.from(result.resolveMap.entries())); + expect(xlLine).toEqual("--typography-weight-arial-regular: 500;"); + expect(result.css).toMatchSnapshot(); + expect(Array.from(result.resolveMap.entries())).toMatchSnapshot(); }); diff --git a/packages/cssforge/tests/version-artifact.test.ts b/packages/cssforge/tests/version-artifact.test.ts index 1a27e7e..d859f9b 100644 --- a/packages/cssforge/tests/version-artifact.test.ts +++ b/packages/cssforge/tests/version-artifact.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; -import { assert, assertEquals, Deno } from "./vitest-compat.ts"; +import { expect, test } from "vitest"; const packageRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); const workspaceRoot = resolve(packageRoot, "../.."); @@ -23,20 +23,20 @@ const run = ( env: NodeJS.ProcessEnv = childEnv, ): { status: number | null; stdout: string; stderr: string } => { const result = spawnSync(command, args, { cwd, encoding: "utf8", env }); - assert( + expect( result.error === undefined, `${command} could not start: ${result.error?.message}`, - ); + ).toBeTruthy(); return { status: result.status, stdout: result.stdout, stderr: result.stderr }; }; const readVersion = (path: string): string => { const parsed: unknown = JSON.parse(readFileSync(path, "utf8")); - assert( + expect( typeof parsed === "object" && parsed !== null && "version" in parsed, `${path} must declare a version`, - ); + ).toBeTruthy(); return String((parsed as { version: unknown }).version); }; @@ -47,10 +47,10 @@ const withVersion = (source: string, version: string): string => { /("version"\s*:\s*")[^"]*(")/, (_match, prefix: string, suffix: string) => `${prefix}${version}${suffix}`, ); - assert( + expect( updated !== source || source.includes(`"${version}"`), "no version field to replace", - ); + ).toBeTruthy(); JSON.parse(updated); return updated; @@ -60,15 +60,18 @@ const withVersion = (source: string, version: string): string => { const parsePackFilename = (output: string): string => { const parsed: unknown = JSON.parse(output); const entries = Array.isArray(parsed) ? parsed : Object.values(parsed ?? {}); - assert(entries.length === 1, `npm pack did not report exactly one tarball: ${output}`); + expect( + entries.length === 1, + `npm pack did not report exactly one tarball: ${output}`, + ).toBeTruthy(); const [entry] = entries; - assert( + expect( typeof entry === "object" && entry !== null && typeof (entry as { filename?: unknown }).filename === "string", `npm pack did not report a filename: ${output}`, - ); + ).toBeTruthy(); return (entry as { filename: string }).filename; }; @@ -77,11 +80,10 @@ const assertSucceeded = ( result: { status: number | null; stdout: string; stderr: string }, description: string, ): void => { - assertEquals( + expect( result.status, - 0, `${description} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, - ); + ).toEqual(0); }; /** @@ -129,12 +131,12 @@ const buildCli = async (): Promise<{ cli: string; cleanUp: () => Promise } } const cli = join(outDir, "cli.js"); - assert(existsSync(cli), `${cli} is missing after the build`); + expect(existsSync(cli), `${cli} is missing after the build`).toBeTruthy(); return { cli, cleanUp: () => rm(outDir, { recursive: true, force: true }) }; }; -Deno.test("cli - reports the manifest version verbatim, with and without CI", async () => { +test("cli - reports the manifest version verbatim, with and without CI", async () => { const manifestVersion = readVersion(join(packageRoot, "package.json")); const workDir = await mkdtemp(join(tmpdir(), "cssforge-built-cli-")); const { cli, cleanUp } = await buildCli(); @@ -151,11 +153,10 @@ Deno.test("cli - reports the manifest version verbatim, with and without CI", as ] as const) { const result = run(process.execPath, [cli, "--version"], packageRoot, env); assertSucceeded(result, `cssforge --version in ${description}`); - assertEquals( + expect( result.stdout.trim(), - manifestVersion, `cssforge --version in ${description} must print the manifest version verbatim`, - ); + ).toEqual(manifestVersion); } // citty prints `meta.version` through its own usage output, where a @@ -163,10 +164,10 @@ Deno.test("cli - reports the manifest version verbatim, with and without CI", as // answered by the CLI itself. const help = run(process.execPath, [cli, "--help"], packageRoot, childEnv); assertSucceeded(help, "cssforge --help"); - assert( + expect( help.stdout.includes(manifestVersion), `cssforge --help must print the manifest version, got:\n${help.stdout}`, - ); + ).toBeTruthy(); // A version flag next to other arguments stays a normal command, which // is how citty resolves its own version flag. @@ -190,30 +191,30 @@ Deno.test("cli - reports the manifest version verbatim, with and without CI", as { ...childEnv, CI: "1" }, ); assertSucceeded(combined, "cssforge --version with other arguments"); - assert( + expect( !combined.stdout.split("\n").some((line) => line.trim() === manifestVersion), "a version flag combined with other arguments must not print the version", - ); - assert( + ).toBeTruthy(); + expect( (await readFile(cssOutput, "utf8")).includes("--spacing-size-2"), "a version flag combined with other arguments must still run the command", - ); + ).toBeTruthy(); } finally { await rm(workDir, { recursive: true, force: true }); await cleanUp(); } }, 180_000); -Deno.test("cli - a packed artifact reports the version its manifest declares", async () => { +test("cli - a packed artifact reports the version its manifest declares", async () => { const workDir = await mkdtemp(join(tmpdir(), "cssforge-artifact-version-")); const workspace = join(workDir, "workspace"); try { copyWorkspace(workspace); - assert( + expect( !existsSync(join(workspace, "node_modules")), "the copied workspace must not carry node_modules", - ); + ).toBeTruthy(); // Write the fixture release version into the copy's metadata. This is // what `changeset version` does to the real workspace, so the generated @@ -249,10 +250,10 @@ Deno.test("cli - a packed artifact reports the version its manifest declares", a assertSucceeded(packed, "npm pack with the fixture version"); const filename = parsePackFilename(packed.stdout); - assert( + expect( filename.includes(fixtureVersion), `the packed tarball ${filename} does not carry the fixture version`, - ); + ).toBeTruthy(); const tarball = join(workDir, filename); const consumer = join(workDir, "consumer"); @@ -277,20 +278,18 @@ Deno.test("cli - a packed artifact reports the version its manifest declares", a const installedRoot = join(consumer, "node_modules", "@hebilicious/cssforge"); const installedVersion = readVersion(join(installedRoot, "package.json")); - assertEquals( + expect( installedVersion, - fixtureVersion, "the installed artifact does not carry the fixture version", - ); + ).toEqual(fixtureVersion); const installedCli = join(installedRoot, "dist", "cli.js"); const reported = run(process.execPath, [installedCli, "--version"], consumer); assertSucceeded(reported, "the installed artifact --version"); - assertEquals( + expect( reported.stdout.trim(), - fixtureVersion, "the installed artifact reports a version that differs from its package.json", - ); + ).toEqual(fixtureVersion); const executable = join( consumer, @@ -298,21 +297,23 @@ Deno.test("cli - a packed artifact reports the version its manifest declares", a ".bin", process.platform === "win32" ? "cssforge.cmd" : "cssforge", ); - assert(existsSync(executable), "npm did not expose the cssforge executable"); + expect( + existsSync(executable), + "npm did not expose the cssforge executable", + ).toBeTruthy(); const viaExecutable = run(executable, ["--version"], consumer); assertSucceeded(viaExecutable, "the installed cssforge executable --version"); - assertEquals( + expect( viaExecutable.stdout.trim(), - fixtureVersion, "the installed cssforge executable reports a stale version", - ); + ).toEqual(fixtureVersion); } finally { await rm(workDir, { recursive: true, force: true }); } }, 240_000); -Deno.test("cli - rejects metadata that disagrees with the package version", async () => { +test("cli - rejects metadata that disagrees with the package version", async () => { const workDir = await mkdtemp(join(tmpdir(), "cssforge-version-check-")); const workspace = join(workDir, "workspace"); @@ -331,14 +332,14 @@ Deno.test("cli - rejects metadata that disagrees with the package version", asyn // The gate must observe drift rather than repair it, so it is never // allowed to run the writing mode first. const rejected = check(); - assert( + expect( rejected.status !== 0, "the consistency check accepted a jsr.json that disagrees with package.json", - ); - assert( + ).toBeTruthy(); + expect( `${rejected.stderr}${rejected.stdout}`.includes("jsr.json declares 0.9.9"), `the consistency check did not explain the disagreement: ${rejected.stderr}`, - ); + ).toBeTruthy(); // The generated module is the channel JSR publishes, so drift there must // be rejected too. @@ -352,16 +353,16 @@ Deno.test("cli - rejects metadata that disagrees with the package version", asyn ), ); const staleGenerated = check(); - assert( + expect( staleGenerated.status !== 0, "the consistency check accepted a generated module that disagrees with package.json", - ); - assert( + ).toBeTruthy(); + expect( `${staleGenerated.stderr}${staleGenerated.stdout}`.includes( "src/version.ts declares 9.9.9", ), `the consistency check did not explain the generated-module drift: ${staleGenerated.stderr}`, - ); + ).toBeTruthy(); // The published tarball ships this changelog, so a release without a // matching entry must not pass. @@ -373,14 +374,14 @@ Deno.test("cli - rejects metadata that disagrees with the package version", asyn changelog.replace(/^## .+$/m, "## 0.0.0-no-entry-for-this-version"), ); const missingEntry = check(); - assert( + expect( missingEntry.status !== 0, "the consistency check accepted a release with no matching changelog entry", - ); - assert( + ).toBeTruthy(); + expect( `${missingEntry.stderr}${missingEntry.stdout}`.includes('has no "## '), `the consistency check did not explain the changelog drift: ${missingEntry.stderr}`, - ); + ).toBeTruthy(); } finally { await rm(workDir, { recursive: true, force: true }); } diff --git a/packages/cssforge/tests/vitest-compat.ts b/packages/cssforge/tests/vitest-compat.ts deleted file mode 100644 index 22daf33..0000000 --- a/packages/cssforge/tests/vitest-compat.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { TestContext } from "vitest"; -import { expect, test } from "vitest"; - -export const Deno = { - test, -}; - -export const assert = (condition: unknown, message?: string) => { - expect(Boolean(condition), message).toBe(true); -}; - -export const assertEquals = (actual: T, expected: T, message?: string) => { - expect(actual, message).toEqual(expected); -}; - -export const assertSnapshot = async (_ctx: TestContext, value: unknown) => { - expect(value).toMatchSnapshot(); -}; - -/** Asserts that `run` throws and returns the Error so the caller can inspect it. */ -export const assertThrows = (run: () => unknown, message?: string): Error => { - try { - run(); - } catch (error) { - expect(error, message).toBeInstanceOf(Error); - return error as Error; - } - - throw new Error("Expected the call to throw, but it returned a value."); -}; - -/** Asserts that `run` does not throw. */ -export const assertDoesNotThrow = (run: () => unknown, message?: string) => { - expect(run, message).not.toThrow(); -}; diff --git a/packages/cssforge/tests/watch.test.ts b/packages/cssforge/tests/watch.test.ts index d9d7a65..3ce318f 100644 --- a/packages/cssforge/tests/watch.test.ts +++ b/packages/cssforge/tests/watch.test.ts @@ -2,8 +2,8 @@ import { spawn } from "node:child_process"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { expect, test } from "vitest"; import { childEnv } from "./helpers.ts"; -import { assert, assertEquals, Deno } from "./vitest-compat.ts"; const configSource = `import { size } from "./tokens.ts"; @@ -96,7 +96,7 @@ console.log(JSON.stringify({ afterTokenChange, afterConfigChange, failureSeen, a process.exit(0); `; -Deno.test("cli - watch mode regenerates the output when a token module or the config changes", async () => { +test("cli - watch mode regenerates the output when a token module or the config changes", async () => { const dir = await mkdtemp(join(tmpdir(), "cssforge-watch-deps-")); try { @@ -128,7 +128,7 @@ Deno.test("cli - watch mode regenerates the output when a token module or the co child.on("close", resolve); }); - assertEquals(exitCode, 0, `watch() failed: ${stderr}`); + expect(exitCode, `watch() failed: ${stderr}`).toEqual(0); const output = stdout .split("\n") @@ -148,27 +148,27 @@ Deno.test("cli - watch mode regenerates the output when a token module or the co afterRecovery: string; }; - assert( + expect( afterTokenChange.includes("--spacing-size-2: 2rem"), `the rebuild must use the new token value: ${afterTokenChange}`, - ); - assert( + ).toBeTruthy(); + expect( !afterTokenChange.includes("0.5rem"), `the rebuild must not keep the old token value: ${afterTokenChange}`, - ); - assert( + ).toBeTruthy(); + expect( afterConfigChange.includes("--spacing-size-4: 1rem"), `a second config change must regenerate too: ${afterConfigChange}`, - ); - assert( + ).toBeTruthy(); + expect( afterConfigChange.includes("--spacing-size-2: 2rem"), `the second rebuild must keep the token module value: ${afterConfigChange}`, - ); - assert(failureSeen, "the invalid config must trigger a rebuild"); - assert( + ).toBeTruthy(); + expect(failureSeen, "the invalid config must trigger a rebuild").toBeTruthy(); + expect( afterRecovery.includes("--spacing-size-2: 9rem"), `a failed rebuild must not stop watching: ${afterRecovery}`, - ); + ).toBeTruthy(); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/packages/docs/agents.md b/packages/docs/agents.md index 5f1e273..8dd387e 100644 --- a/packages/docs/agents.md +++ b/packages/docs/agents.md @@ -37,7 +37,7 @@ Each release publishes the same version to npm and to JSR. - Import: `import { defineConfig } from "@hebilicious/cssforge";` - Run: the package exposes a `cssforge` executable, so run it directly (`cssforge --mode all`, or `npx cssforge --mode all` with npm) -- Deno: `deno add jsr:@hebilicious/cssforge`, then `deno run -A jsr:@hebilicious/cssforge/cli` +- JSR: `npx jsr add @hebilicious/cssforge` or `pnpm i jsr:@hebilicious/cssforge` - Runtime: Node 24 or newer, which loads `cssforge.config.ts` without a loader or `tsx` ## Agent workflow diff --git a/skills/cssforge/SKILL.md b/skills/cssforge/SKILL.md index d0ba496..fe5b94d 100644 --- a/skills/cssforge/SKILL.md +++ b/skills/cssforge/SKILL.md @@ -35,8 +35,8 @@ Do not invent schema fields that are not present in docs or source types. - Mirror patterns from README configuration sections. 3. Configure generation commands -- Install the package from npm: `npm install --save-dev @hebilicious/cssforge` (Deno projects - use the secondary JSR channel: `deno add jsr:@hebilicious/cssforge`). +- Install the package from npm: `npm install --save-dev @hebilicious/cssforge` (the same + version is on the secondary JSR channel: `npx jsr add @hebilicious/cssforge`). - Run the installed executable directly: `cssforge --mode all` (`npx cssforge` with npm, `pnpm cssforge` with pnpm) - For local repo examples, call the built CLI of the sibling package directly: pnpm only diff --git a/skills/cssforge/references/cli-reference.md b/skills/cssforge/references/cli-reference.md index 1be8775..2797dc3 100644 --- a/skills/cssforge/references/cli-reference.md +++ b/skills/cssforge/references/cli-reference.md @@ -35,10 +35,6 @@ with npm, `pnpm cssforge` with pnpm): - Extra color formats: - `npx cssforge --mode all --color-formats hex,rgb` -Deno projects run the same CLI from the secondary JSR channel: - -- `deno run -A jsr:@hebilicious/cssforge/cli --mode all` - ## Programmatic API From `README.md`: