From 834bc729c8e1a4fa321de61477ab74a208895a36 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 6 Feb 2026 19:27:26 -0500 Subject: [PATCH 01/17] chore: docs testing with vitest --- .github/workflows/ci.yml | 2 +- docs/sum.test.ts | 6 + docs/sum.ts | 3 + package-lock.json | 906 ++++++++++++++++++++++++++++++++++++++- package.json | 5 +- vitest.config.ts | 7 + 6 files changed, 926 insertions(+), 3 deletions(-) create mode 100644 docs/sum.test.ts create mode 100644 docs/sum.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44905bee..d43ab65f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,4 +30,4 @@ jobs: npm run check:types - name: Test run: | - npm test + npm test && npm run test:docs diff --git a/docs/sum.test.ts b/docs/sum.test.ts new file mode 100644 index 00000000..b64a155b --- /dev/null +++ b/docs/sum.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from 'vitest'; +import { sum } from './sum.ts'; + +test('that 1 + 2 equals 3', () => { + expect(sum(1, 2)).toBe(3); +}); diff --git a/docs/sum.ts b/docs/sum.ts new file mode 100644 index 00000000..7372ed02 --- /dev/null +++ b/docs/sum.ts @@ -0,0 +1,3 @@ +export function sum(a: number, b: number): number { + return a + b; +} diff --git a/package-lock.json b/package-lock.json index 682b3691..92fe6185 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,8 @@ "stylelint": "^16.10.0", "stylelint-config-recommended": "^14.0.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.2" + "typescript-eslint": "^8.46.2", + "vitest": "^4.0.18" }, "engines": { "node": ">=18" @@ -274,6 +275,448 @@ "url": "https://github.com/sponsors/JounQin" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1228,6 +1671,13 @@ "node": ">=6" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -1272,6 +1722,27 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/chai/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/@types/co-body": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", @@ -1323,6 +1794,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1790,6 +2268,137 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -3879,6 +4488,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-module-shims": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.10.1.tgz", @@ -3915,6 +4531,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4305,6 +4963,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9595,6 +10263,17 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9964,6 +10643,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -11363,6 +12049,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", @@ -11499,6 +12192,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -11509,6 +12209,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12119,6 +12826,23 @@ "node": ">=0.10.0" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -12135,6 +12859,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -12882,6 +13616,159 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -13029,6 +13916,23 @@ "dev": true, "license": "ISC" }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", diff --git a/package.json b/package.json index 1af6caea..5a0a8e34 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,8 @@ "test:jsx": "c8 node --import ./test-register.js --experimental-strip-types ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"", "test:tdd": "npm run test -- --watch", "test:tdd:jsx": "npm run test:jsx -- --watch", + "test:docs": "vitest run", + "test:docs:tdd": "vitest", "prepare": "husky" }, "dependencies": { @@ -96,6 +98,7 @@ "stylelint": "^16.10.0", "stylelint-config-recommended": "^14.0.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.2" + "typescript-eslint": "^8.46.2", + "vitest": "^4.0.18" } } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..e48c41f9 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['./docs/**/*.test.ts'], + }, +}); From e533d2ef10b4b2935a804d1af8387bff6e55e403 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 6 Feb 2026 20:02:01 -0500 Subject: [PATCH 02/17] chore: vanilla web component browser test --- docs/components/greeting/greeting.test.ts | 43 ++++++ docs/components/greeting/greeting.ts | 9 ++ package-lock.json | 161 ++++++++++++++++++++++ package.json | 2 + vitest.config.ts | 7 + 5 files changed, 222 insertions(+) create mode 100644 docs/components/greeting/greeting.test.ts create mode 100644 docs/components/greeting/greeting.ts diff --git a/docs/components/greeting/greeting.test.ts b/docs/components/greeting/greeting.test.ts new file mode 100644 index 00000000..de11ce68 --- /dev/null +++ b/docs/components/greeting/greeting.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './greeting.ts'; + +describe('Components/Greeting', () => { + let greeting: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(async () => { + greeting = document.createElement('wcc-greeting'); + + document.body.appendChild(greeting); + }); + + it('should not be undefined', () => { + expect(greeting).not.equal(undefined); + expect(greeting.querySelectorAll('span').length).equal(1); + }); + + it('should render the default greeting text', () => { + expect(greeting.querySelector('span').textContent).equal('Hello World!'); + }); + }); + + describe('Passing Name Attribute', () => { + const name = 'WCC'; + + beforeEach(async () => { + greeting = document.createElement('wcc-greeting'); + greeting.setAttribute('name', name); + + document.body.appendChild(greeting); + }); + + it('should render the default greeting text', () => { + expect(greeting.querySelector('span').textContent).equal(`Hello ${name}!`); + }); + }); + + afterEach(() => { + greeting.remove(); + greeting = undefined; + }); +}); diff --git a/docs/components/greeting/greeting.ts b/docs/components/greeting/greeting.ts new file mode 100644 index 00000000..cb4742c7 --- /dev/null +++ b/docs/components/greeting/greeting.ts @@ -0,0 +1,9 @@ +export default class GreetingComponent extends HTMLElement { + connectedCallback() { + const name = this.hasAttribute('name') ? this.getAttribute('name') : 'World'; + + this.innerHTML = `Hello ${name}!`; + } +} + +customElements.define('wcc-greeting', GreetingComponent); diff --git a/package-lock.json b/package-lock.json index 92fe6185..055cedfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@mapbox/rehype-prism": "^0.8.0", "@types/mocha": "^10.0.10", "@types/node": "^22.13.4", + "@vitest/browser-playwright": "^4.0.18", "c8": "^7.11.2", "chai": "^4.3.6", "eslint": "^9.39.1", @@ -40,6 +41,7 @@ "lint-staged": "^16.2.6", "mocha": "^9.2.2", "open-props": "^1.7.4", + "playwright": "^1.58.2", "prettier": "^3.6.2", "prism-themes": "^1.9.0", "prismjs": "^1.28.0", @@ -1204,6 +1206,13 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@projectevergreen/acorn-jsx-esm": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@projectevergreen/acorn-jsx-esm/-/acorn-jsx-esm-0.1.0.tgz", @@ -2268,6 +2277,53 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitest/browser": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz", + "integrity": "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/mocker": "4.0.18", + "@vitest/utils": "4.0.18", + "magic-string": "^0.30.21", + "pixelmatch": "7.1.0", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.0.3", + "ws": "^8.18.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz", + "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.0.18", + "@vitest/mocker": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -10063,6 +10119,16 @@ "semver": "bin/semver.js" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10711,6 +10777,76 @@ "node": ">= 6" } }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -12070,6 +12206,21 @@ "dev": true, "license": "ISC" }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12945,6 +13096,16 @@ "dev": true, "license": "MIT" }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", diff --git a/package.json b/package.json index 5a0a8e34..d5d1f27d 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@mapbox/rehype-prism": "^0.8.0", "@types/mocha": "^10.0.10", "@types/node": "^22.13.4", + "@vitest/browser-playwright": "^4.0.18", "c8": "^7.11.2", "chai": "^4.3.6", "eslint": "^9.39.1", @@ -87,6 +88,7 @@ "lint-staged": "^16.2.6", "mocha": "^9.2.2", "open-props": "^1.7.4", + "playwright": "^1.58.2", "prettier": "^3.6.2", "prism-themes": "^1.9.0", "prismjs": "^1.28.0", diff --git a/vitest.config.ts b/vitest.config.ts index e48c41f9..2a546561 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,14 @@ import { defineConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; export default defineConfig({ test: { include: ['./docs/**/*.test.ts'], + browser: { + provider: playwright(), + enabled: true, + headless: true, + instances: [{ browser: 'chromium' }], + }, }, }); From c254a3a38b25ea0b86957d701ecba0a4f7806605 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 6 Feb 2026 20:04:42 -0500 Subject: [PATCH 03/17] chore: docs specific github action --- .github/workflows/ci-docs.yml | 24 ++++++++++++++++++++++++ .github/workflows/ci.yml | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci-docs.yml diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml new file mode 100644 index 00000000..d30f1a20 --- /dev/null +++ b/.github/workflows/ci-docs.yml @@ -0,0 +1,24 @@ +name: Continuous Integration (Docs) + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node: [22] + + steps: + - uses: actions/checkout@v6 + - name: Use Node.js ${{ matrix.node }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + - name: Installing project dependencies + run: | + npm ci + - name: Test Docs + run: | + npm run test:docs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d43ab65f..44905bee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,4 +30,4 @@ jobs: npm run check:types - name: Test run: | - npm test && npm run test:docs + npm test From d68754d80af263e08f6f82acace48c1cb55431e1 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 6 Feb 2026 20:07:03 -0500 Subject: [PATCH 04/17] chore: setup playwright for docs CI --- .github/workflows/ci-docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index d30f1a20..38ee43cd 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -19,6 +19,9 @@ jobs: - name: Installing project dependencies run: | npm ci + - name: Setup Playwright + run: | + npx playwright install - name: Test Docs run: | npm run test:docs From 8c3dc2e89d5c78681de6b39255ca2d2a30b17944 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 6 Feb 2026 20:58:34 -0500 Subject: [PATCH 05/17] chore: full component testing example --- docs/components/footer/footer.test.ts | 56 +++++++++++++++++++++++++++ docs/components/footer/footer.tsx | 1 - docs/sum.test.ts | 6 --- docs/sum.ts | 3 -- package-lock.json | 1 + package.json | 1 + vitest.config.ts | 56 +++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 docs/components/footer/footer.test.ts delete mode 100644 docs/sum.test.ts delete mode 100644 docs/sum.ts diff --git a/docs/components/footer/footer.test.ts b/docs/components/footer/footer.test.ts new file mode 100644 index 00000000..29b5fa43 --- /dev/null +++ b/docs/components/footer/footer.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './footer.tsx'; + +describe('Components/Footer', () => { + let footer: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(async () => { + footer = document.createElement('wcc-footer'); + + document.body.appendChild(footer); + }); + + it('should not be undefined', () => { + expect(footer).not.equal(undefined); + expect(footer.querySelectorAll('footer').length).equal(1); + }); + + it('should have a link for to the home page', () => { + const homeLink = footer.querySelectorAll('a[title="WCC Home Page"]'); + + expect(homeLink.length).equal(1); + expect(homeLink[0].getAttribute('href')).equal('/'); + }); + + it('should have the WCC logo inside the home page link', () => { + const logo = footer.querySelectorAll('a[title="WCC Home Page"] > svg'); + + expect(logo.length).equal(1); + }); + + it('should have one instance of the social tray component', () => { + const socialTray = footer.querySelectorAll('wcc-social-tray'); + + expect(socialTray.length).equal(1); + }); + }); + // const name = 'WCC'; + + // beforeEach(async () => { + // greeting = document.createElement('wcc-greeting'); + // greeting.setAttribute('name', name); + + // document.body.appendChild(greeting); + // }); + + // it('should render the default greeting text', () => { + // expect(greeting.querySelector('span').textContent).equal(`Hello ${name}!`); + // }); + // }); + + afterEach(() => { + footer.remove(); + footer = undefined; + }); +}); diff --git a/docs/components/footer/footer.tsx b/docs/components/footer/footer.tsx index 7d58ce9a..58484c9a 100644 --- a/docs/components/footer/footer.tsx +++ b/docs/components/footer/footer.tsx @@ -13,7 +13,6 @@ export default class Footer extends HTMLElement { {wccLogo} -
diff --git a/docs/sum.test.ts b/docs/sum.test.ts deleted file mode 100644 index b64a155b..00000000 --- a/docs/sum.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from 'vitest'; -import { sum } from './sum.ts'; - -test('that 1 + 2 equals 3', () => { - expect(sum(1, 2)).toBe(3); -}); diff --git a/docs/sum.ts b/docs/sum.ts deleted file mode 100644 index 7372ed02..00000000 --- a/docs/sum.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function sum(a: number, b: number): number { - return a + b; -} diff --git a/package-lock.json b/package-lock.json index 055cedfb..041909fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "stylelint-config-recommended": "^14.0.1", "typescript": "^5.9.3", "typescript-eslint": "^8.46.2", + "vite": "^7.3.1", "vitest": "^4.0.18" }, "engines": { diff --git a/package.json b/package.json index d5d1f27d..0e1db4cd 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "stylelint-config-recommended": "^14.0.1", "typescript": "^5.9.3", "typescript-eslint": "^8.46.2", + "vite": "^7.3.1", "vitest": "^4.0.18" } } diff --git a/vitest.config.ts b/vitest.config.ts index 2a546561..a7d060a4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,59 @@ import { defineConfig } from 'vitest/config'; import { playwright } from '@vitest/browser-playwright'; +import { parseJsx } from './src/jsx-loader.js'; +import { generate } from 'astring'; // comes from @greenwood/plugin-import-jsx +import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; +// @ts-expect-error +import { readAndMergeConfig } from '@greenwood/cli/src/lifecycles/config.js'; +// @ts-expect-error +import { initContext } from '@greenwood/cli/src/lifecycles/context.js'; +import fs from 'node:fs/promises'; +import type { Plugin } from 'vite'; + +// initialize Greenwood context and plugins +const config = await readAndMergeConfig(); +const context = await initContext({ config }); +const compilation = { context, config }; +// @ts-expect-error +const rawResource = greenwoodPluginImportRaw()[0].provider(compilation); + +function transformJsx(): Plugin { + return { + name: 'transform-jsx', + enforce: 'pre', + transform: (src, id) => { + if (id.endsWith('.tsx') || id.endsWith('.jsx')) { + const tree = parseJsx(new URL(`file://${id}`)); + const contents = generate(tree); + + return { + code: contents, + map: null, + }; + } + }, + }; +} + +function transformRawImports(): Plugin { + return { + name: 'transform-raw-imports', + enforce: 'pre', + transform: async (src, id) => { + if (id.endsWith('?type=raw')) { + const url = new URL(`file://${id}`); + const contents = await fs.readFile(url, 'utf-8'); + const response = await rawResource.intercept(url, null, new Response(contents)); + const body = await response.text(); + + return { + code: body, + map: null, + }; + } + }, + }; +} export default defineConfig({ test: { @@ -9,6 +63,8 @@ export default defineConfig({ enabled: true, headless: true, instances: [{ browser: 'chromium' }], + screenshotFailures: false, }, }, + plugins: [transformJsx(), transformRawImports()], }); From 4c22e2d98ba00c7e1880c2a29107d612e6d5533a Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 6 Feb 2026 20:59:37 -0500 Subject: [PATCH 06/17] chore: full component testing example --- docs/components/footer/footer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/components/footer/footer.tsx b/docs/components/footer/footer.tsx index 58484c9a..7d58ce9a 100644 --- a/docs/components/footer/footer.tsx +++ b/docs/components/footer/footer.tsx @@ -13,6 +13,7 @@ export default class Footer extends HTMLElement { {wccLogo} +
From dc29675fac0ed55c77c099968f384c037486d4b2 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 6 Feb 2026 21:01:10 -0500 Subject: [PATCH 07/17] chore: full component testing example --- docs/components/footer/footer.test.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docs/components/footer/footer.test.ts b/docs/components/footer/footer.test.ts index 29b5fa43..943af884 100644 --- a/docs/components/footer/footer.test.ts +++ b/docs/components/footer/footer.test.ts @@ -35,19 +35,6 @@ describe('Components/Footer', () => { expect(socialTray.length).equal(1); }); }); - // const name = 'WCC'; - - // beforeEach(async () => { - // greeting = document.createElement('wcc-greeting'); - // greeting.setAttribute('name', name); - - // document.body.appendChild(greeting); - // }); - - // it('should render the default greeting text', () => { - // expect(greeting.querySelector('span').textContent).equal(`Hello ${name}!`); - // }); - // }); afterEach(() => { footer.remove(); From ebda63acb207e0f960bae7c2ab483ea7bc3081ca Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 7 Feb 2026 09:43:37 -0500 Subject: [PATCH 08/17] chore: social tray component unit tests --- .../social-tray/social-tray.test.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/components/social-tray/social-tray.test.ts diff --git a/docs/components/social-tray/social-tray.test.ts b/docs/components/social-tray/social-tray.test.ts new file mode 100644 index 00000000..589d60bd --- /dev/null +++ b/docs/components/social-tray/social-tray.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './social-tray.tsx'; + +const ICONS = [ + { + link: 'https://github.com/ProjectEvergreen/wcc', + title: 'GitHub', + }, + { + link: 'https://www.greenwoodjs.dev/discord/', + title: 'Discord', + }, + { + link: 'https://bsky.app/profile/projectevergreen.bsky.social', + title: 'BlueSky', + }, + { + link: 'https://twitter.com/PrjEvergreen', + title: 'Twitter', + }, +]; + +describe('Components/Social Tray', () => { + let tray: HTMLElement; + + beforeEach(async () => { + tray = document.createElement('wcc-social-tray'); + + document.body.appendChild(tray); + }); + + describe('Default Behavior', () => { + it('should not be null', () => { + expect(tray).not.equal(undefined); + expect(tray.querySelectorAll('ul').length).equal(1); + }); + + it('should have the expected social link icons', () => { + const links = tray.querySelectorAll('ul li a'); + const icons = tray.querySelectorAll('ul li a svg'); + const noShowScreenReader = tray.querySelectorAll('ul li a span.no-show-screen-reader'); + + expect(links.length).to.equal(4); + expect(icons.length).to.equal(4); + expect(noShowScreenReader.length).to.equal(4); + + Array.from(links).forEach((link) => { + const iconItem = ICONS.find((icon) => icon.title === link.getAttribute('title')); + + expect(iconItem).to.not.equal(undefined); + expect(link.getAttribute('href')).to.equal(iconItem.link); + expect(link.getAttribute('target')).equal('_blank'); + }); + }); + }); + + afterEach(() => { + tray.remove(); + tray = undefined; + }); +}); From 9514ff8f9bd7bbb6d0bfe0d859561927fc4bd966 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 7 Feb 2026 09:54:14 -0500 Subject: [PATCH 09/17] chore: vitest coverage thresholds --- package-lock.json | 123 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- vitest.config.ts | 11 +++++ 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 041909fb..17d0a40f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^22.13.4", "@vitest/browser-playwright": "^4.0.18", + "@vitest/coverage-v8": "^4.0.18", "c8": "^7.11.2", "chai": "^4.3.6", "eslint": "^9.39.1", @@ -83,6 +84,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", @@ -93,6 +104,36 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -2325,6 +2366,47 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -2789,6 +2871,35 @@ "node": "*" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -8592,6 +8703,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", diff --git a/package.json b/package.json index 0e1db4cd..0cb3f9ef 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "test:jsx": "c8 node --import ./test-register.js --experimental-strip-types ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"", "test:tdd": "npm run test -- --watch", "test:tdd:jsx": "npm run test:jsx -- --watch", - "test:docs": "vitest run", + "test:docs": "vitest run --coverage", "test:docs:tdd": "vitest", "prepare": "husky" }, @@ -76,6 +76,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^22.13.4", "@vitest/browser-playwright": "^4.0.18", + "@vitest/coverage-v8": "^4.0.18", "c8": "^7.11.2", "chai": "^4.3.6", "eslint": "^9.39.1", diff --git a/vitest.config.ts b/vitest.config.ts index a7d060a4..c420b036 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -65,6 +65,17 @@ export default defineConfig({ instances: [{ browser: 'chromium' }], screenshotFailures: false, }, + coverage: { + provider: 'v8', + include: ['./docs/components/**'], + exclude: ['./docs/components/sandbox/**'], + // thresholds: { + // lines: 80, + // functions: 80, + // branches: 80, + // statements: 80, + // } + }, }, plugins: [transformJsx(), transformRawImports()], }); From 1b9ab3d8295552f2d501ee4268df8c6d83b2db39 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 11 Feb 2026 10:04:49 -0500 Subject: [PATCH 10/17] chore: header with active content test cases --- .github/workflows/ci-docs.yml | 2 +- CONTRIBUTING.md | 6 ++ docs/components/header/header.test.ts | 134 ++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 docs/components/header/header.test.ts diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index 38ee43cd..06619c79 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -24,4 +24,4 @@ jobs: npx playwright install - name: Test Docs run: | - npm run test:docs + npm run build && npm run test:docs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0344567f..f71957b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,7 @@ To develop for the project, you'll want to follow these steps: 1. Clone the repository 1. Have [NodeJS LTS](https://nodejs.org) installed and / or use `nvm` (see below) 1. Run `npm ci` +1. Run `npx playwright install` ### NVM @@ -48,6 +49,11 @@ The website is built with [**Greenwood**](https://www.greenwoodjs.dev). To run t - `npm run build` - Generate a production build - `npm run serve` - Serve a production build +To run tests for the website, first make sure you have at least run `npm run build`, then: + +- `npm run test:docs` - Run a single suite of tests with Vitest +- `npm run test:docs:tdd` - Run the test suite with Vitest in watch mode + ### Sandbox To assist in local development of WCC, there is a "sandbox" app built into the website, that can be used to validate a number of examples in the browser. (think of it as a storybook for WCC). diff --git a/docs/components/header/header.test.ts b/docs/components/header/header.test.ts new file mode 100644 index 00000000..4ca71ef4 --- /dev/null +++ b/docs/components/header/header.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import pages from '../../../.greenwood/graph.json' with { type: 'json' }; +import './header.tsx'; +import type { Page } from '@greenwood/cli'; + +const CURRENT_ROUTE = '/docs/'; + +window.fetch = function () { + return new Promise((resolve) => { + resolve(new Response(JSON.stringify(pages.filter((page) => page.data.collection === 'nav')))); + }); +}; + +describe('Components/Header', () => { + let header: HTMLElement; + + beforeEach(async () => { + header = document.createElement('wcc-header'); + header.setAttribute('current-route', CURRENT_ROUTE); + + document.body.appendChild(header); + + // to support async connected callback usage + await vi.waitUntil(() => header.querySelector('header')); + }); + + describe('Default Behavior', () => { + it('should not be null', () => { + expect(header).not.equal(undefined); + expect(header.querySelectorAll('header').length).equal(1); + }); + + it('should have an anchor tag with title attribute wrapping the logo', () => { + const anchor = header.querySelector("a[title='WCC Home Page']"); + + expect(anchor).to.not.equal(undefined); + expect(anchor.getAttribute('href')).to.equal('/'); + }); + + it('should have the WCC logo', () => { + const logo = header.querySelectorAll("a[title='WCC Home Page'] svg"); + + expect(logo.length).equal(1); + expect(logo[0]).not.equal(undefined); + }); + + it('should have the expected desktop navigation links', () => { + const links = header.querySelectorAll("nav[aria-label='Main'] ul li a"); + let activeRoute: Page; + + Array.from(links).forEach((link, idx) => { + const navItem = pages.find((nav) => nav.route === link.getAttribute('href')); + + expect(navItem).to.not.equal(undefined); + expect(navItem.data.order).to.equal((idx += 1)); + expect(link.textContent).to.equal(navItem.label); + + // Home page doesn't have a title, for example + // maybe a Greenwood bug? + if (navItem.title) { + expect(link.getAttribute('title')).to.equal(navItem.title); + } + + // current route should display as active + if (navItem.route === CURRENT_ROUTE && link.getAttribute('class').includes('active')) { + activeRoute = navItem; + } + }); + + expect(activeRoute.route).to.equal(CURRENT_ROUTE); + }); + }); + + describe('Mobile Menu', () => { + const popoverTarget = 'mobile-menu'; + + it('should have the expected mobile menu icon button', () => { + const mobileIconButton = header.querySelectorAll( + "button[aria-label='Mobile Menu Icon Button']", + ); + + expect(mobileIconButton.length).to.equal(1); + expect(mobileIconButton[0].getAttribute('popovertarget')).to.equal(popoverTarget); + }); + + it('should have the expected popover overlay container', () => { + const overlay = header.querySelectorAll(`#${popoverTarget}`); + + expect(overlay.length).to.equal(1); + expect(overlay[0].getAttribute('popover')).to.equal('manual'); + }); + + it('should have the expected close button', () => { + const mobileCloseButton = header.querySelectorAll( + "button[aria-label='Mobile Menu Close Button']", + ); + + expect(mobileCloseButton.length).to.equal(1); + expect(mobileCloseButton[0].getAttribute('popovertarget')).to.equal(popoverTarget); + expect(mobileCloseButton[0].getAttribute('popovertargetaction')).to.equal('hide'); + }); + + it('should have the expected navigation links', () => { + const links = header.querySelectorAll("nav[aria-label='Mobile'] ul li a"); + let activeRoute: Page = undefined; + + Array.from(links).forEach((link, idx) => { + const navItem = pages.find((nav) => nav.route === link.getAttribute('href')); + + expect(navItem).to.not.equal(undefined); + expect(navItem.data.order).to.equal((idx += 1)); + expect(link.textContent).to.equal(navItem.label); + + // Home page doesn't have a title, for example + // maybe a Greenwood bug? + if (navItem.title) { + expect(link.getAttribute('title')).to.equal(navItem.title); + } + + // current route should display as active + if (navItem.route === CURRENT_ROUTE && link.getAttribute('class').includes('active')) { + activeRoute = navItem; + } + }); + + expect(activeRoute.route).to.equal(CURRENT_ROUTE); + }); + }); + + afterEach(() => { + header.remove(); + header = undefined; + }); +}); From 6e2a1e171d3b62633c0bd6f696163ba356d8ab53 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 11 Feb 2026 10:08:17 -0500 Subject: [PATCH 11/17] chore: fix github actions ordering --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44905bee..b05803f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: - name: Lint run: | npm run lint + - name: Build + run: | + npm run build - name: Check Types run: | npm run check:types From 303f0a3b34418a3a062184536165e68627263aad Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 11 Feb 2026 21:25:32 -0500 Subject: [PATCH 12/17] chore: adjust github actions --- .github/workflows/ci-docs.yml | 9 ++++++++- .github/workflows/ci-win.yml | 2 +- .github/workflows/ci.yml | 8 +------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index 06619c79..f533a146 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -22,6 +22,13 @@ jobs: - name: Setup Playwright run: | npx playwright install + - name: Build + - name: Build the website + run: | + npm run build + - name: Check Types + run: | + npm run check:types - name: Test Docs run: | - npm run build && npm run test:docs + npm run test:docs diff --git a/.github/workflows/ci-win.yml b/.github/workflows/ci-win.yml index 9e45999a..234a1320 100644 --- a/.github/workflows/ci-win.yml +++ b/.github/workflows/ci-win.yml @@ -21,4 +21,4 @@ jobs: npm ci - name: Test run: | - npm test + npm run test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b05803f5..9faf5fe4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,12 +25,6 @@ jobs: - name: Lint run: | npm run lint - - name: Build - run: | - npm run build - - name: Check Types - run: | - npm run check:types - name: Test run: | - npm test + npm run test From fdb4370a94ebe300ab8039ecb37f4c87545d4864 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 11 Feb 2026 21:32:14 -0500 Subject: [PATCH 13/17] chore: playwright support in github actions --- .github/workflows/ci-docs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index f533a146..7e3f8939 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -21,8 +21,7 @@ jobs: npm ci - name: Setup Playwright run: | - npx playwright install - - name: Build + npx playwright install --with-deps - name: Build the website run: | npm run build From 1f015dae033c5c29f91a399df01f98c9bca48cdc Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 11 Feb 2026 23:00:07 -0500 Subject: [PATCH 14/17] chore: add more test cases --- docs/components/ctc-button/ctc-button.test.ts | 31 +++ docs/components/greeting/greeting.test.ts | 43 ---- docs/components/greeting/greeting.ts | 9 - docs/components/header/header.test.ts | 2 +- .../side-nav.module.css} | 0 docs/components/side-nav/side-nav.test.ts | 201 ++++++++++++++++++ .../sidenav.tsx => side-nav/side-nav.tsx} | 16 +- docs/layouts/docs.html | 4 +- docs/layouts/examples.html | 5 +- docs/styles/docs.css | 4 +- tsconfig.json | 3 +- vitest.config.ts | 47 +++- 12 files changed, 289 insertions(+), 76 deletions(-) create mode 100644 docs/components/ctc-button/ctc-button.test.ts delete mode 100644 docs/components/greeting/greeting.test.ts delete mode 100644 docs/components/greeting/greeting.ts rename docs/components/{sidenav/sidenav.module.css => side-nav/side-nav.module.css} (100%) create mode 100644 docs/components/side-nav/side-nav.test.ts rename docs/components/{sidenav/sidenav.tsx => side-nav/side-nav.tsx} (85%) diff --git a/docs/components/ctc-button/ctc-button.test.ts b/docs/components/ctc-button/ctc-button.test.ts new file mode 100644 index 00000000..b2cd9aee --- /dev/null +++ b/docs/components/ctc-button/ctc-button.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './ctc-button.tsx'; + +describe('Components/Copy To Clipboard (Button)', () => { + const content = 'npm i -D wc-compiler'; + let ctc: HTMLElement; + + beforeEach(async () => { + ctc = document.createElement('wcc-ctc-button'); + + ctc.setAttribute('content', content); + document.body.appendChild(ctc); + }); + + describe('Default Behavior', () => { + it('should not be null', () => { + expect(ctc).not.equal(undefined); + }); + + it('should have an icon with the user provided content set', () => { + const icon = ctc.shadowRoot.querySelectorAll("[title='Copy to clipboard']"); + + expect(icon.length).to.equal(1); + }); + }); + + afterEach(() => { + ctc.remove(); + ctc = undefined; + }); +}); diff --git a/docs/components/greeting/greeting.test.ts b/docs/components/greeting/greeting.test.ts deleted file mode 100644 index de11ce68..00000000 --- a/docs/components/greeting/greeting.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import './greeting.ts'; - -describe('Components/Greeting', () => { - let greeting: HTMLElement; - - describe('Default Behavior', () => { - beforeEach(async () => { - greeting = document.createElement('wcc-greeting'); - - document.body.appendChild(greeting); - }); - - it('should not be undefined', () => { - expect(greeting).not.equal(undefined); - expect(greeting.querySelectorAll('span').length).equal(1); - }); - - it('should render the default greeting text', () => { - expect(greeting.querySelector('span').textContent).equal('Hello World!'); - }); - }); - - describe('Passing Name Attribute', () => { - const name = 'WCC'; - - beforeEach(async () => { - greeting = document.createElement('wcc-greeting'); - greeting.setAttribute('name', name); - - document.body.appendChild(greeting); - }); - - it('should render the default greeting text', () => { - expect(greeting.querySelector('span').textContent).equal(`Hello ${name}!`); - }); - }); - - afterEach(() => { - greeting.remove(); - greeting = undefined; - }); -}); diff --git a/docs/components/greeting/greeting.ts b/docs/components/greeting/greeting.ts deleted file mode 100644 index cb4742c7..00000000 --- a/docs/components/greeting/greeting.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default class GreetingComponent extends HTMLElement { - connectedCallback() { - const name = this.hasAttribute('name') ? this.getAttribute('name') : 'World'; - - this.innerHTML = `Hello ${name}!`; - } -} - -customElements.define('wcc-greeting', GreetingComponent); diff --git a/docs/components/header/header.test.ts b/docs/components/header/header.test.ts index 4ca71ef4..4da63eff 100644 --- a/docs/components/header/header.test.ts +++ b/docs/components/header/header.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import pages from '../../../.greenwood/graph.json' with { type: 'json' }; -import './header.tsx'; import type { Page } from '@greenwood/cli'; +import './header.tsx'; const CURRENT_ROUTE = '/docs/'; diff --git a/docs/components/sidenav/sidenav.module.css b/docs/components/side-nav/side-nav.module.css similarity index 100% rename from docs/components/sidenav/sidenav.module.css rename to docs/components/side-nav/side-nav.module.css diff --git a/docs/components/side-nav/side-nav.test.ts b/docs/components/side-nav/side-nav.test.ts new file mode 100644 index 00000000..abe1abaf --- /dev/null +++ b/docs/components/side-nav/side-nav.test.ts @@ -0,0 +1,201 @@ +import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest'; +import type { DocsPage } from './side-nav.tsx'; +import './side-nav.tsx'; + +const ROUTE = '/docs/'; +const HEADING = 'Docs'; + +// have to hardcode this since tableOfContents data is not set in the graph +const GRAPH = [ + { + id: 'docs', + label: 'Docs', + title: 'Docs', + route: '/docs/', + layout: 'docs', + data: { + collection: 'nav', + order: 2, + tocHeading: 2, + tableOfContents: [ + { + content: 'API', + slug: 'api', + lvl: 2, + i: 1, + seen: 0, + }, + { + content: 'Metadata', + slug: 'metadata', + lvl: 2, + i: 4, + seen: 0, + }, + { + content: 'Progressive Hydration', + slug: 'progressive-hydration', + lvl: 2, + i: 5, + seen: 0, + }, + { + content: 'Data', + slug: 'data', + lvl: 2, + i: 6, + seen: 0, + }, + { + content: 'Conventions', + slug: 'conventions', + lvl: 2, + i: 9, + seen: 0, + }, + { + content: 'TypeScript', + slug: 'typescript', + lvl: 2, + i: 10, + seen: 0, + }, + { + content: 'JSX', + slug: 'jsx', + lvl: 2, + i: 12, + seen: 0, + }, + ], + }, + }, +]; + +// https://stackoverflow.com/questions/45425169/intercept-fetch-api-requests-and-responses-in-javascript +window.fetch = function () { + return new Promise((resolve) => { + resolve(new Response(JSON.stringify(GRAPH))); + }); +}; + +describe('Components/Side Nav', () => { + let nav: HTMLElement; + let expectedDocsContent: DocsPage; + + beforeEach(async () => { + nav = document.createElement('wcc-side-nav'); + nav.setAttribute('route', ROUTE); + nav.setAttribute('heading', HEADING); + + expectedDocsContent = GRAPH.find((page) => page.route === ROUTE); + + document.body.appendChild(nav); + + // to support async connected callback usage + await vi.waitUntil(() => nav.querySelector('nav')); + }); + + describe('Default Behavior - Main Menu', () => { + let fullMenu: HTMLElement; + + beforeEach(async () => { + fullMenu = nav.querySelector('#main-menu'); + }); + + it('should not be null', () => { + expect(fullMenu).not.equal(undefined); + }); + + it('should have the expected ToC heading', () => { + const heading = fullMenu.querySelectorAll('p'); + + expect(heading.length).to.equal(1); + expect(heading[0].textContent).to.equal('Table of Contents'); + }); + + it('should have the expected number of section heading links', () => { + const links = fullMenu.querySelectorAll('ul li a'); + + expect(links.length).to.equal(expectedDocsContent.data.tableOfContents.length); + }); + + it('should have the expected content for section headings links', () => { + const links = fullMenu.querySelectorAll('ul li a'); + + links.forEach((link, index) => { + expect(link.textContent).to.equal(expectedDocsContent.data.tableOfContents[index].content); + }); + }); + + afterAll(() => { + fullMenu = null; + }); + }); + + describe('Default Behavior - Mobile Menu', () => { + let compactMenu: HTMLElement; + let popoverSelector = 'compact-menu'; + + beforeEach(async () => { + compactMenu = nav.querySelector(`#mobile-menu`); + }); + + it('should not be null', () => { + expect(compactMenu).not.equal(undefined); + }); + + it('should have the expected ToC heading', () => { + const heading = compactMenu.querySelectorAll('p'); + + expect(heading.length).to.equal(1); + expect(heading[0].textContent).to.equal('Table of Contents'); + }); + + it('should have the expected popover trigger element', () => { + const trigger = compactMenu.querySelectorAll( + `[popovertarget="${popoverSelector}"]:not([popovertargetaction])`, + ); + + expect(trigger.length).to.equal(1); + expect(trigger[0].textContent).to.contain(HEADING); + }); + + it('should have the expected popover element', () => { + const popover = compactMenu.querySelectorAll(`[popover="manual"]`); + + expect(popover.length).to.equal(1); + }); + + it('should have the expected popover close button', () => { + const closeButton = compactMenu.querySelectorAll( + `[popover="manual"] [popovertarget="${popoverSelector}"]`, + ); + + expect(closeButton.length).to.equal(1); + }); + + it('should have the expected number of section heading links', () => { + const links = compactMenu.querySelectorAll('ul li a'); + + expect(links.length).to.equal(expectedDocsContent.data.tableOfContents.length); + }); + + it('should have the expected content for section headings links', () => { + const links = compactMenu.querySelectorAll('ul li a'); + + links.forEach((link, index) => { + expect(link.textContent).to.equal(expectedDocsContent.data.tableOfContents[index].content); + }); + }); + + afterEach(() => { + compactMenu = null; + }); + }); + + afterEach(() => { + nav.remove(); + nav = undefined; + }); +}); diff --git a/docs/components/sidenav/sidenav.tsx b/docs/components/side-nav/side-nav.tsx similarity index 85% rename from docs/components/sidenav/sidenav.tsx rename to docs/components/side-nav/side-nav.tsx index 6d820cae..109055ed 100644 --- a/docs/components/sidenav/sidenav.tsx +++ b/docs/components/side-nav/side-nav.tsx @@ -1,13 +1,13 @@ import type { Page, Graph } from '@greenwood/cli'; import { getContent } from '@greenwood/cli/src/data/client.js'; -import styles from './sidenav.module.css'; +import styles from './side-nav.module.css'; -type TableOfContents = Array<{ +export type TableOfContents = Array<{ content: string; slug: string; }>; -type DocsPage = Page & { +export type DocsPage = Page & { data?: { tableOfContents?: TableOfContents; }; @@ -38,13 +38,13 @@ export default class SideNav extends HTMLElement { .join(''); return ( -
-
+
+ ); } } -customElements.define('wcc-sidenav', SideNav); +customElements.define('wcc-side-nav', SideNav); diff --git a/docs/layouts/docs.html b/docs/layouts/docs.html index 0a3b74c8..3b1ff7d9 100644 --- a/docs/layouts/docs.html +++ b/docs/layouts/docs.html @@ -2,11 +2,11 @@ WCC - ${globalThis.page.title} - + - +
diff --git a/docs/layouts/examples.html b/docs/layouts/examples.html index e8c71e05..7ffe8d33 100644 --- a/docs/layouts/examples.html +++ b/docs/layouts/examples.html @@ -2,12 +2,11 @@ WCC - ${globalThis.page.title} - + - - +
diff --git a/docs/styles/docs.css b/docs/styles/docs.css index 9fc60d51..cedf0b99 100644 --- a/docs/styles/docs.css +++ b/docs/styles/docs.css @@ -1,4 +1,4 @@ -wcc-sidenav { +wcc-side-nav { display: block; margin: var(--size-4) auto; } @@ -89,7 +89,7 @@ body:has(#compact-menu:popover-open) { font-size: var(--font-size-1); } - wcc-sidenav { + wcc-side-nav { display: inline-block; width: 20%; min-width: 25%; diff --git a/tsconfig.json b/tsconfig.json index f845e8f9..3d64ae0a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "types": ["mocha", "node"], "jsx": "preserve", "jsxImportSource": "wc-compiler", - "lib": ["DOM"] + "lib": ["DOM"], + "resolveJsonModule": true }, "exclude": ["node_modules"] } diff --git a/vitest.config.ts b/vitest.config.ts index c420b036..dc501351 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,10 +4,13 @@ import { parseJsx } from './src/jsx-loader.js'; import { generate } from 'astring'; // comes from @greenwood/plugin-import-jsx import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; // @ts-expect-error +import { greenwoodPluginStandardCss } from '@greenwood/cli/src/plugins/resource/plugin-standard-css.js'; +// @ts-expect-error import { readAndMergeConfig } from '@greenwood/cli/src/lifecycles/config.js'; // @ts-expect-error import { initContext } from '@greenwood/cli/src/lifecycles/context.js'; import fs from 'node:fs/promises'; +import path from 'node:path'; import type { Plugin } from 'vite'; // initialize Greenwood context and plugins @@ -16,6 +19,7 @@ const context = await initContext({ config }); const compilation = { context, config }; // @ts-expect-error const rawResource = greenwoodPluginImportRaw()[0].provider(compilation); +const standardCssResource = greenwoodPluginStandardCss.provider(compilation); function transformJsx(): Plugin { return { @@ -35,6 +39,41 @@ function transformJsx(): Plugin { }; } +function transformConstructableStylesheetsPlugin(): Plugin { + return { + name: 'transform-constructable-stylesheets', + enforce: 'pre', + resolveId: (id, importer) => { + if ( + // you'll need to configure this `importer` line to the location of your own components + importer?.indexOf('/docs/components/') >= 0 && + id.endsWith('.css') && + !id.endsWith('.module.css') + ) { + // append .type to the end of Constructable Stylesheet file paths so that they are not automatically precessed by Vite's default pipeline + return path.join(path.dirname(importer), `${id}.type`); + } + }, + load: async (id) => { + if (id.endsWith('.css.type')) { + const filename = id.slice(0, -5); + const contents = await fs.readFile(filename, 'utf-8'); + const url = new URL(`file://${id.replace('.type', '')}`); + // "coerce" native constructable stylesheets into inline JS so Vite / Rollup do not complain + const request = new Request(url, { + headers: { + Accept: 'text/javascript', + }, + }); + const response = await standardCssResource.intercept(url, request, new Response(contents)); + const body = await response.text(); + + return body; + } + }, + }; +} + function transformRawImports(): Plugin { return { name: 'transform-raw-imports', @@ -69,13 +108,7 @@ export default defineConfig({ provider: 'v8', include: ['./docs/components/**'], exclude: ['./docs/components/sandbox/**'], - // thresholds: { - // lines: 80, - // functions: 80, - // branches: 80, - // statements: 80, - // } }, }, - plugins: [transformJsx(), transformRawImports()], + plugins: [transformJsx(), transformRawImports(), transformConstructableStylesheetsPlugin()], }); From 72bdb1f02e4056878931b3a9de706dfdb6fc47b4 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 13 Feb 2026 10:03:03 -0500 Subject: [PATCH 15/17] chore: moar test cases --- docs/components/banner-cta/banner-cta.test.ts | 48 ++++++++++++ .../banner-splash/banner-splash.test.ts | 30 ++++++++ .../capability-box/capability-box.test.ts | 61 ++++++++++++++++ .../capability-box/capability-box.tsx | 4 +- .../feature-box/feature-box.test.ts | 73 +++++++++++++++++++ docs/components/feature-box/feature-box.tsx | 8 +- vitest.config.ts | 6 ++ 7 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 docs/components/banner-cta/banner-cta.test.ts create mode 100644 docs/components/banner-splash/banner-splash.test.ts create mode 100644 docs/components/capability-box/capability-box.test.ts create mode 100644 docs/components/feature-box/feature-box.test.ts diff --git a/docs/components/banner-cta/banner-cta.test.ts b/docs/components/banner-cta/banner-cta.test.ts new file mode 100644 index 00000000..4ca1ecdd --- /dev/null +++ b/docs/components/banner-cta/banner-cta.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './banner-cta.tsx'; + +describe('Components/Banner CTA', () => { + let bannerCta: HTMLElement; + + describe('Default Behavior', () => { + const cta = 'npm i -D wc-compiler'; + + beforeEach(() => { + bannerCta = document.createElement('wcc-banner-cta'); + + document.body.appendChild(bannerCta); + }); + + it('should not be undefined', () => { + expect(bannerCta).not.equal(undefined); + }); + + it('should have the main overview text', () => { + const paragraph = bannerCta.querySelectorAll('p'); + + expect(paragraph.length).equal(1); + expect(paragraph[0].textContent.trim().replace(/\s+/g, ' ')).toContain( + 'WCC (WC Compiler) is a NodeJS based package for server-rendering native Web Components.', + ); + }); + + it('should have the copy to clipboard snippet', () => { + const pre = bannerCta.querySelectorAll('pre'); + + expect(pre.length).equal(1); + expect(pre[0].textContent.trim()).equal(`$ ${cta}`); + }); + + it('should have the copy to clipboard component', () => { + const ctcButton = bannerCta.querySelectorAll('wcc-ctc-button'); + + expect(ctcButton.length).equal(1); + expect(ctcButton[0].getAttribute('content')).equal(cta); + }); + }); + + afterEach(() => { + bannerCta.remove(); + bannerCta = undefined; + }); +}); diff --git a/docs/components/banner-splash/banner-splash.test.ts b/docs/components/banner-splash/banner-splash.test.ts new file mode 100644 index 00000000..b5ad7a2b --- /dev/null +++ b/docs/components/banner-splash/banner-splash.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './banner-splash.tsx'; + +describe('Components/Banner Splash', () => { + let bannerSplash: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(() => { + bannerSplash = document.createElement('wcc-banner-splash'); + + document.body.appendChild(bannerSplash); + }); + + it('should not be undefined', () => { + expect(bannerSplash).not.equal(undefined); + }); + + it('should have to the expected heading text', () => { + const heading = bannerSplash.querySelectorAll('h1'); + + expect(heading.length).equal(1); + expect(heading[0].textContent.trim().replace(/\s+/g, ' ')).equal('SSR for Web Components'); + }); + }); + + afterEach(() => { + bannerSplash.remove(); + bannerSplash = undefined; + }); +}); diff --git a/docs/components/capability-box/capability-box.test.ts b/docs/components/capability-box/capability-box.test.ts new file mode 100644 index 00000000..f335899b --- /dev/null +++ b/docs/components/capability-box/capability-box.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './capability-box.tsx'; + +describe('Components/Capability Box', () => { + let capabilityBox: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(() => { + capabilityBox = document.createElement('wcc-capability-box'); + + document.body.appendChild(capabilityBox); + }); + + it('should not be undefined', () => { + expect(capabilityBox).not.equal(undefined); + }); + + it('should not have any heading text', () => { + const heading = capabilityBox.querySelectorAll('h4'); + + expect(heading.length).equal(1); + expect(heading[0].textContent).equal(''); + }); + }); + + describe('Custom Text and Heading', () => { + const customHeading = 'JSX'; + const customContent = 'This is a custom feature box'; + + beforeEach(() => { + capabilityBox = document.createElement('wcc-capability-box'); + capabilityBox.innerHTML = `

${customContent}

`; + capabilityBox.setAttribute('heading', customHeading); + + document.body.appendChild(capabilityBox); + }); + + it('should not be undefined', () => { + expect(capabilityBox).not.equal(undefined); + }); + + it('should have the expected text from setting the heading attribute', () => { + const heading = capabilityBox.querySelectorAll('h4'); + + expect(heading.length).equal(1); + expect(heading[0].textContent).equal(customHeading); + }); + + it('should have the expected inner HTML text', () => { + const paragraph = capabilityBox.querySelectorAll('p'); + + expect(paragraph.length).equal(1); + expect(paragraph[0].textContent).equal(customContent); + }); + }); + + afterEach(() => { + capabilityBox.remove(); + capabilityBox = undefined; + }); +}); diff --git a/docs/components/capability-box/capability-box.tsx b/docs/components/capability-box/capability-box.tsx index 7d56cf51..d598130f 100644 --- a/docs/components/capability-box/capability-box.tsx +++ b/docs/components/capability-box/capability-box.tsx @@ -6,12 +6,12 @@ export default class CapabilityBox extends HTMLElement { } render() { - const heading = this.getAttribute('heading'); + const heading = this.getAttribute('heading') ?? ''; const { innerHTML } = this; return (
- {heading} +

{heading}

{innerHTML}
); diff --git a/docs/components/feature-box/feature-box.test.ts b/docs/components/feature-box/feature-box.test.ts new file mode 100644 index 00000000..afff6f72 --- /dev/null +++ b/docs/components/feature-box/feature-box.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './feature-box.tsx'; + +describe('Components/Feature Box', () => { + let featureBox: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(() => { + featureBox = document.createElement('wcc-feature-box'); + + document.body.appendChild(featureBox); + }); + + it('should not be undefined', () => { + expect(featureBox).not.equal(undefined); + }); + + it('should not have any heading text', () => { + const heading = featureBox.querySelectorAll('h4'); + + expect(heading.length).equal(1); + expect(heading[0].textContent.trim()).equal(''); + }); + + it('should not have an SVG mapped logo', () => { + const logo = featureBox.querySelectorAll('svg'); + + expect(logo.length).equal(0); + }); + }); + + describe('Custom Text and Heading', () => { + const customHeading = 'JSX'; + const customContent = 'This is a custom feature box'; + + beforeEach(() => { + featureBox = document.createElement('wcc-feature-box'); + featureBox.innerHTML = `

${customContent}

`; + featureBox.setAttribute('heading', customHeading); + + document.body.appendChild(featureBox); + }); + + it('should not be undefined', () => { + expect(featureBox).not.equal(undefined); + }); + + it('should have the expected text from setting the heading attribute', () => { + const heading = featureBox.querySelectorAll('h4'); + + expect(heading.length).equal(1); + expect(heading[0].textContent.trim()).equal(customHeading); + }); + + it('should have the expected inner HTML text', () => { + const paragraph = featureBox.querySelectorAll('p'); + + expect(paragraph.length).equal(1); + expect(paragraph[0].textContent).equal(customContent); + }); + + it('should not a mapped SVG logo', () => { + const logo = featureBox.querySelectorAll('svg'); + + expect(logo.length).equal(1); + }); + }); + + afterEach(() => { + featureBox.remove(); + featureBox = undefined; + }); +}); diff --git a/docs/components/feature-box/feature-box.tsx b/docs/components/feature-box/feature-box.tsx index 7cb42e4b..0c920436 100644 --- a/docs/components/feature-box/feature-box.tsx +++ b/docs/components/feature-box/feature-box.tsx @@ -15,16 +15,16 @@ export default class FeatureBox extends HTMLElement { } render() { - const heading = this.getAttribute('heading'); + const heading = this.getAttribute('heading') ?? ''; const { innerHTML } = this; - const icon = FeatureBox.ICON_MAPPER[heading]; + const icon = FeatureBox.ICON_MAPPER[heading] ?? ''; return (
- +

{icon} {heading} - +

{innerHTML}
); diff --git a/vitest.config.ts b/vitest.config.ts index dc501351..627823d0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -108,6 +108,12 @@ export default defineConfig({ provider: 'v8', include: ['./docs/components/**'], exclude: ['./docs/components/sandbox/**'], + thresholds: { + lines: 65, + functions: 75, + branches: 50, + statements: 65, + }, }, }, plugins: [transformJsx(), transformRawImports(), transformConstructableStylesheetsPlugin()], From fb8e4214ba86182654ea3f2c6e516e9df8ae6f7c Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 13 Feb 2026 10:31:16 -0500 Subject: [PATCH 16/17] chore: fetch mocking for active content test cases --- docs/components/header/header.test.ts | 23 ++++++++++++++++------- docs/components/side-nav/side-nav.test.ts | 20 +++++++++++++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/docs/components/header/header.test.ts b/docs/components/header/header.test.ts index 4da63eff..8acf8c93 100644 --- a/docs/components/header/header.test.ts +++ b/docs/components/header/header.test.ts @@ -1,19 +1,23 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi, beforeAll, afterAll } from 'vitest'; import pages from '../../../.greenwood/graph.json' with { type: 'json' }; import type { Page } from '@greenwood/cli'; import './header.tsx'; const CURRENT_ROUTE = '/docs/'; -window.fetch = function () { - return new Promise((resolve) => { - resolve(new Response(JSON.stringify(pages.filter((page) => page.data.collection === 'nav')))); - }); -}; - describe('Components/Header', () => { let header: HTMLElement; + beforeAll(() => { + window.fetch = vi.fn((): Promise => { + return new Promise((resolve) => { + resolve( + new Response(JSON.stringify(pages.filter((page) => page.data.collection === 'nav'))), + ); + }); + }); + }); + beforeEach(async () => { header = document.createElement('wcc-header'); header.setAttribute('current-route', CURRENT_ROUTE); @@ -131,4 +135,9 @@ describe('Components/Header', () => { header.remove(); header = undefined; }); + + afterAll(() => { + vi.clearAllMocks(); + vi.resetAllMocks(); + }); }); diff --git a/docs/components/side-nav/side-nav.test.ts b/docs/components/side-nav/side-nav.test.ts index abe1abaf..b27252e6 100644 --- a/docs/components/side-nav/side-nav.test.ts +++ b/docs/components/side-nav/side-nav.test.ts @@ -72,17 +72,18 @@ const GRAPH = [ }, ]; -// https://stackoverflow.com/questions/45425169/intercept-fetch-api-requests-and-responses-in-javascript -window.fetch = function () { - return new Promise((resolve) => { - resolve(new Response(JSON.stringify(GRAPH))); - }); -}; - describe('Components/Side Nav', () => { let nav: HTMLElement; let expectedDocsContent: DocsPage; + beforeAll(() => { + window.fetch = vi.fn((): Promise => { + return new Promise((resolve) => { + resolve(new Response(JSON.stringify(GRAPH))); + }); + }); + }); + beforeEach(async () => { nav = document.createElement('wcc-side-nav'); nav.setAttribute('route', ROUTE); @@ -198,4 +199,9 @@ describe('Components/Side Nav', () => { nav.remove(); nav = undefined; }); + + afterAll(() => { + vi.clearAllMocks(); + vi.resetAllMocks(); + }); }); From 76adbf6e2422615427b8ca84452c0d857dd990c3 Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Fri, 13 Feb 2026 20:39:31 -0500 Subject: [PATCH 17/17] chore: remove resolveJsonModules from tsconfig --- tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 3d64ae0a..f845e8f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,7 @@ "types": ["mocha", "node"], "jsx": "preserve", "jsxImportSource": "wc-compiler", - "lib": ["DOM"], - "resolveJsonModule": true + "lib": ["DOM"] }, "exclude": ["node_modules"] }