From 7194df03f4fd2996a550b1f720b033e7ba9e84ee Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 2 Dec 2025 20:56:56 +0300 Subject: [PATCH 01/49] =?UTF-8?q?Fix=20flaky=20E2E=20test=20cases=20-=20sw?= =?UTF-8?q?ap=20every=20self-import=20in=20that=20module=20to=20relative?= =?UTF-8?q?=20./=E2=80=A6=20paths=20so=20the=20only=20alias=20use=20that?= =?UTF-8?q?=20remains=20is=20from=20the=20spec=20files=20themselves,=20ext?= =?UTF-8?q?end=20BasePage.waitForPageLoad()=20with=20a=20requireNext=20opt?= =?UTF-8?q?ion=20so=20callers=20can=20specifically=20wait=20for=20the=20ne?= =?UTF-8?q?xt=20astro:page-load=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _TODO.md | 5 ++++ test/e2e/helpers/index.ts | 24 +++++++++---------- test/e2e/helpers/pageObjectModels/BasePage.ts | 12 ++++++---- .../pageObjectModels/BreadCrumbPage.ts | 2 +- .../e2e/specs/01-smoke/critical-paths.spec.ts | 4 ++-- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/_TODO.md b/_TODO.md index 677af9f25..e4e53b5c4 100644 --- a/_TODO.md +++ b/_TODO.md @@ -1,5 +1,10 @@ # TODO +Next, I'd like to add a "pause" and "play" icon to src/components/Animations/Computers +There are icons with those names already configured for the Icon component. +There are hooks for pause and play already setup in the component. +The icon should be displayed in the low right hand corner of the animation, with 4px of padding from the bottom and right side. It should overlay the animation, not expand the bounding box of the animation. + ## Performance Implement mitigations in test/e2e/specs/07-performance/PERFORMANCE.md diff --git a/test/e2e/helpers/index.ts b/test/e2e/helpers/index.ts index 115e9c804..662557e13 100644 --- a/test/e2e/helpers/index.ts +++ b/test/e2e/helpers/index.ts @@ -2,31 +2,31 @@ export { test, describe, expect, -} from '@test/e2e/helpers/baseTest' +} from './baseTest' export { setupConsoleErrorChecker, logConsoleErrors, -} from '@test/e2e/helpers/consoleErrors' +} from './consoleErrors' export { setupConsoleCapture, printCapturedMessages, -} from '@test/e2e/helpers/consoleCapture' -export { clearConsentCookies } from '@test/e2e/helpers/browserState' -export { BasePage } from '@test/e2e/helpers/pageObjectModels/BasePage' -export { ComponentPersistencePage } from '@test/e2e/helpers/pageObjectModels/ComponentPersistencePage' -export { HeadPage } from '@test/e2e/helpers/pageObjectModels/HeadPage' -export { BreadCrumbPage } from '@test/e2e/helpers/pageObjectModels/BreadCrumbPage' +} from './consoleCapture' +export { clearConsentCookies } from './browserState' +export { BasePage } from './pageObjectModels/BasePage' +export { ComponentPersistencePage } from './pageObjectModels/ComponentPersistencePage' +export { HeadPage } from './pageObjectModels/HeadPage' +export { BreadCrumbPage } from './pageObjectModels/BreadCrumbPage' export { spyOnFetchEndpoint, mockFetchEndpointResponse, injectHeadersIntoFetch, delayFetchForEndpoint, -} from '@test/e2e/helpers/fetchOverride' -export type { FetchOverrideHandle } from '@test/e2e/helpers/fetchOverride' +} from './fetchOverride' +export type { FetchOverrideHandle } from './fetchOverride' export { setupCleanTestPage, setupTestPage, selectTheme, getThemePickerToggle, -} from '@test/e2e/helpers/cookieHelper' -export { wiremock, mocksEnabled } from '@test/e2e/helpers/mockServices' +} from './cookieHelper' +export { wiremock, mocksEnabled } from './mockServices' diff --git a/test/e2e/helpers/pageObjectModels/BasePage.ts b/test/e2e/helpers/pageObjectModels/BasePage.ts index d7108b29f..e0e20c084 100644 --- a/test/e2e/helpers/pageObjectModels/BasePage.ts +++ b/test/e2e/helpers/pageObjectModels/BasePage.ts @@ -435,18 +435,22 @@ export class BasePage { * await page.waitForPageLoad() * ``` */ - async waitForPageLoad(): Promise { + async waitForPageLoad(options?: { requireNext?: boolean; timeout?: number }): Promise { + const requireNext = options?.requireNext ?? false + const timeout = options?.timeout ?? DEFAULT_NAVIGATION_TIMEOUT const currentCount = await this._page.evaluate(() => window.__astroPageLoadCounter ?? 0) - if (currentCount > this.lastAstroPageLoadCount) { + if (!requireNext && currentCount > this.lastAstroPageLoadCount) { this.lastAstroPageLoadCount = currentCount return } + const baseline = requireNext ? currentCount : this.lastAstroPageLoadCount + await this._page.waitForFunction( previousCount => (window.__astroPageLoadCounter ?? 0) > previousCount, - currentCount, - { timeout: DEFAULT_NAVIGATION_TIMEOUT } + baseline, + { timeout } ) this.lastAstroPageLoadCount = await this._page.evaluate(() => window.__astroPageLoadCounter ?? 0) diff --git a/test/e2e/helpers/pageObjectModels/BreadCrumbPage.ts b/test/e2e/helpers/pageObjectModels/BreadCrumbPage.ts index 50ba2cd93..4eb3cfcf2 100644 --- a/test/e2e/helpers/pageObjectModels/BreadCrumbPage.ts +++ b/test/e2e/helpers/pageObjectModels/BreadCrumbPage.ts @@ -55,7 +55,7 @@ export class BreadCrumbPage extends BasePage { return } - const waitForLoad = this.waitForPageLoad() + const waitForLoad = this.waitForPageLoad({ requireNext: true }) await this.click(`a[href="${targetHref}"]`) await waitForLoad } diff --git a/test/e2e/specs/01-smoke/critical-paths.spec.ts b/test/e2e/specs/01-smoke/critical-paths.spec.ts index a17f6eee6..937f161e0 100644 --- a/test/e2e/specs/01-smoke/critical-paths.spec.ts +++ b/test/e2e/specs/01-smoke/critical-paths.spec.ts @@ -24,7 +24,7 @@ test.describe('Critical Paths @smoke', () => { const page = await BasePage.init(playwrightPage) for (const { url: path } of page.navigationItems) { await page.goto('/') - const navigationComplete = page.waitForPageLoad() + const navigationComplete = page.waitForPageLoad({ requireNext: true }) await page.navigateToPage(path) await navigationComplete await playwrightPage.waitForFunction(() => { @@ -56,7 +56,7 @@ test.describe('Critical Paths @smoke', () => { await playwrightPage.waitForSelector('.menu-visible', { state: 'visible' }) // Click navigation link - const navigationComplete = page.waitForPageLoad() + const navigationComplete = page.waitForPageLoad({ requireNext: true }) await page.click(`a[href="${path}"]`) await navigationComplete await playwrightPage.waitForFunction(() => { From 65927989a566f166d61a0af15be8ac060cb6623c Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 2 Dec 2025 21:15:00 +0300 Subject: [PATCH 02/49] Fix flaky E2E tests - added a devServerPort constant in astro.config.ts (defaults to DEV_SERVER_PORT or 4321) and wired it into vite.server.hmr.clientPort so Vite instructs the browser to negotiate HMR websockets through the same externally exposed port Astro serves on --- astro.config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/astro.config.ts b/astro.config.ts index 98eb0551b..d9f06e412 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -35,6 +35,9 @@ import { privacyPolicyVersion } from './src/integrations/PrivacyPolicyVersion' import { pwaDevAssetServer } from './src/lib/plugins/pwaDevAssetServer' import { createSerializeFunction, pagesJsonWriter } from './src/integrations/sitemapSerialize' +// Ensure Vite's HMR websocket connects through the same exposed dev server port used by Astro. +const devServerPort = Number(process.env['DEV_SERVER_PORT'] ?? 4321) + const sharedTestIntegrations = [ icon(), ] @@ -93,6 +96,11 @@ export default defineConfig({ site: getSiteUrl(), trailingSlash: 'never', vite: { + server: { + hmr: { + clientPort: devServerPort, + }, + }, build: { /** Source map generation must be turned on for Sentry. */ sourcemap: true, From c1902f0c4e15f330d927d36258ad3b761f50b1c2 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 2 Dec 2025 21:35:20 +0300 Subject: [PATCH 03/49] Fix flaky E2E test cases - update BasePage.ts so expectNoErrors() filters out known non-actionable Firefox/Vite HMR websocket noise ('WebSocket closed without opened') before asserting pageErrors().length. Added isIgnorablePageError() helper with documentation. --- e2e-stress-run.sh | 41 +++++++++++++++++++ test/e2e/helpers/pageObjectModels/BasePage.ts | 17 +++++++- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100755 e2e-stress-run.sh diff --git a/e2e-stress-run.sh b/e2e-stress-run.sh new file mode 100755 index 000000000..07180f00e --- /dev/null +++ b/e2e-stress-run.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -uo pipefail + +SUCCESS_FILE="/tmp/e2e-stress-run-success-count" + +if [[ ! -f .env.development ]]; then + echo "Missing .env.development file" >&2 + exit 1 +fi + +# Load dev environment variables so every Playwright run matches local stress conditions. +set -a +source .env.development +set +a + +if [[ ! -f "$SUCCESS_FILE" ]]; then + echo "0" > "$SUCCESS_FILE" +fi + +while true; do + CI=1 FORCE_COLOR=1 E2E_MOCKS=1 npx playwright test + status=$? + + if [[ $status -eq 0 ]]; then + count=$(<"$SUCCESS_FILE") + if ! [[ "$count" =~ ^[0-9]+$ ]]; then + count=0 + fi + count=$((count + 1)) + echo "$count" > "$SUCCESS_FILE" + echo "Number of successful runs: $count" + else + count=$(<"$SUCCESS_FILE") + if ! [[ "$count" =~ ^[0-9]+$ ]]; then + count=0 + fi + echo "Number of successful runs before failure: $count" + exit $status + fi + +done diff --git a/test/e2e/helpers/pageObjectModels/BasePage.ts b/test/e2e/helpers/pageObjectModels/BasePage.ts index e0e20c084..aea37f2ab 100644 --- a/test/e2e/helpers/pageObjectModels/BasePage.ts +++ b/test/e2e/helpers/pageObjectModels/BasePage.ts @@ -862,8 +862,9 @@ export class BasePage { async expectNoErrors(): Promise> { await this.waitForPageComplete() const errors = await this._page.pageErrors() - expect(errors).toHaveLength(0) - return await this._page.pageErrors() + const filteredErrors = errors.filter((error) => !this.isIgnorablePageError(error)) + expect(filteredErrors).toHaveLength(0) + return filteredErrors } /** @@ -1177,4 +1178,16 @@ export class BasePage { await expect(label).toBeVisible() await expect(label).toContainText(pattern) } + + /** + * Filter recurring non-actionable browser errors (e.g., Firefox HMR websockets) from pageErrors(). + */ + private isIgnorablePageError(error: Error): boolean { + const message = error?.message ?? '' + + // Firefox occasionally surfaces this when Vite's HMR websocket retries during stress runs. + if (message.includes('WebSocket closed without opened')) return true + + return false + } } \ No newline at end of file From edb4054af5bc698237a778a3ef2af6a044c84cec Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 2 Dec 2025 21:45:13 +0300 Subject: [PATCH 04/49] =?UTF-8?q?Fixes=20to=20Action=20workflow=20-=20add?= =?UTF-8?q?=20id:=20vitest=20to=20the=20=E2=80=9CRun=20unit=20tests?= =?UTF-8?q?=E2=80=9D=20step=20so=20downstream=20steps=20can=20see=20whethe?= =?UTF-8?q?r=20coverage=20files=20were=20generated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-and-test.yml | 3 ++- e2e-stress-run.sh | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d84a2144c..d2374d0e7 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -57,6 +57,7 @@ jobs: run: npm run lint - name: Run unit tests (Vitest — coverage with GitHub Actions reporter) + id: vitest run: | # Run vitest with coverage. The built-in 'github-actions' reporter # (configured in vitest.config.ts) will create annotations. @@ -65,7 +66,7 @@ jobs: - name: Report Coverage uses: davelosert/vitest-coverage-report-action@v2.9.0 - if: always() + if: ${{ always() && steps.vitest.outcome == 'success' && hashFiles('coverage/coverage-summary.json') != '' }} with: json-summary-path: './coverage/coverage-summary.json' json-final-path: './coverage/coverage-final.json' diff --git a/e2e-stress-run.sh b/e2e-stress-run.sh index 07180f00e..f652f8763 100755 --- a/e2e-stress-run.sh +++ b/e2e-stress-run.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# End-to-end stress test runner script set -uo pipefail SUCCESS_FILE="/tmp/e2e-stress-run-success-count" From 7c10fc60dfa6698fd7676077ed2fb178604bde36 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 2 Dec 2025 21:53:44 +0300 Subject: [PATCH 05/49] Fix Actions workflow - add a dedicated 'Sync Astro types' step to build-and-test.yml so CI runs npm run sync before TypeScript checks, typed rss.xml.ts by importing CollectionEntry and annotating the article parameter in the RSS item map to satisfy noImplicitAny --- .github/workflows/build-and-test.yml | 3 +++ src/pages/rss.xml.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d2374d0e7..fb19290f4 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -50,6 +50,9 @@ jobs: - name: Install dependencies run: npm ci --legacy-peer-deps + - name: Sync Astro types + run: npm run sync + - name: Run TypeScript check run: npm run check diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index a58636530..fcfd1739e 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,6 +1,7 @@ import rss from '@astrojs/rss' import { getCollection } from 'astro:content' import type { APIContext } from 'astro' +import type { CollectionEntry } from 'astro:content' export async function GET(context: APIContext) { const articles = await getCollection('articles') @@ -9,7 +10,7 @@ export async function GET(context: APIContext) { description: 'Webstack Builders is a solo software development agency specializing in platform engineering.', site: context.site ?? 'https://webstackbuilders.com', - items: articles.map(article => ({ + items: articles.map((article: CollectionEntry<'articles'>) => ({ title: article.data.title, pubDate: article.data.publishDate, description: article.data.description, From e80ec57b501d364e4bff93265d3c6e2bfb0cdc0e Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 2 Dec 2025 22:20:57 +0300 Subject: [PATCH 06/49] move __fixtures__ directout to parent of __tests__ --- package.json | 2 +- .../Contact/client/__fixtures__/contactForm.fixture.astro | 5 +++++ .../client/__tests__/__fixtures__/contactForm.fixture.astro | 5 ----- src/components/Forms/Contact/client/__tests__/testUtils.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 src/components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro delete mode 100644 src/components/Forms/Contact/client/__tests__/__fixtures__/contactForm.fixture.astro diff --git a/package.json b/package.json index b9760ffae..7198b7824 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ ], "scripts": { "build": "cross-env NODE_ENV=production npm run lint && npx astro build", - "check": "npm run lint && npx astro check", + "check": "npx astro check", "clean": "FORCE_COLOR=1 npx rimraf dist && FORCE_COLOR=1 npx rimraf .astro", "dev": "npm run sync && cross-env NODE_ENV=development FORCE_COLOR=1 npx astro dev", "dev:env": "FORCE_COLOR=1 dotenv -e .env.development -- npm run dev", diff --git a/src/components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro b/src/components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro new file mode 100644 index 000000000..f99d3ccc5 --- /dev/null +++ b/src/components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro @@ -0,0 +1,5 @@ +--- +import ContactForm from '../../index.astro' +--- + + diff --git a/src/components/Forms/Contact/client/__tests__/__fixtures__/contactForm.fixture.astro b/src/components/Forms/Contact/client/__tests__/__fixtures__/contactForm.fixture.astro deleted file mode 100644 index 148b79449..000000000 --- a/src/components/Forms/Contact/client/__tests__/__fixtures__/contactForm.fixture.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import ContactForm from '../../../index.astro' ---- - - diff --git a/src/components/Forms/Contact/client/__tests__/testUtils.ts b/src/components/Forms/Contact/client/__tests__/testUtils.ts index 400b89ad8..b529c004b 100644 --- a/src/components/Forms/Contact/client/__tests__/testUtils.ts +++ b/src/components/Forms/Contact/client/__tests__/testUtils.ts @@ -1,7 +1,7 @@ import { expect } from 'vitest' import { experimental_AstroContainer as AstroContainer } from 'astro/container' import { TestError } from '@test/errors' -import ContactFormFixture from '@components/Forms/Contact/client/__tests__/__fixtures__/contactForm.fixture.astro' +import ContactFormFixture from '@components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro' import type { ContactFormElement } from '@components/Forms/Contact/client' import { getContactFormElements } from '@components/Forms/Contact/client/selectors' import type { ContactFormElements } from '@components/Forms/Contact/client/@types' From 3e00732030d45f33b43067afa72a7f861de0682e Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 2 Dec 2025 22:25:16 +0300 Subject: [PATCH 07/49] Fix astro check errors --- .../Contact/client/__fixtures__/contactForm.fixture.astro | 4 ++-- src/components/Toasts/NetworkStatus/index.astro | 2 +- src/pages/testing/carousel.astro | 4 ++-- src/pages/testing/consent-preferences.astro | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro b/src/components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro index f99d3ccc5..207392fd0 100644 --- a/src/components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro +++ b/src/components/Forms/Contact/client/__fixtures__/contactForm.fixture.astro @@ -1,5 +1,5 @@ --- -import ContactForm from '../../index.astro' +import ContactFormComponent from '../../index.astro' --- - + diff --git a/src/components/Toasts/NetworkStatus/index.astro b/src/components/Toasts/NetworkStatus/index.astro index 138568bfa..f8c54e4aa 100644 --- a/src/components/Toasts/NetworkStatus/index.astro +++ b/src/components/Toasts/NetworkStatus/index.astro @@ -15,7 +15,7 @@ import styles from './index.module.css' --- diff --git a/src/pages/testing/carousel.astro b/src/pages/testing/carousel.astro index 8ede08d75..09dc5502c 100644 --- a/src/pages/testing/carousel.astro +++ b/src/pages/testing/carousel.astro @@ -1,6 +1,6 @@ --- import BaseLayout from '@layouts/BaseLayout.astro' -import Carousel from '@components/Carousel/index.astro' +import CarouselComponent from '@components/Carousel/index.astro' const pageTitle = 'Carousel Testing Ground' const pageDescription = 'Dedicated page for validating carousel interactions without relying on marketing pages.' @@ -21,7 +21,7 @@ const pagePath = '/testing/carousel'

- - + From 8295e355d11e5b9f65808fdf18f0b62bd1020fce Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 2 Dec 2025 22:42:28 +0300 Subject: [PATCH 08/49] Add heredoc .env file in Build and Test Action workflow --- .github/workflows/build-and-test.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index fb19290f4..02d2e5228 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -88,6 +88,26 @@ jobs: - name: Build Upstash mock image run: docker buildx build --load -t wb/upstash-redis-local:test test/containers/upstash/local-proxy + - name: Generate container env file + shell: bash + run: | + set -euo pipefail + mkdir -p test/containers + : "${SUPABASE_SERVICE_ROLE_KEY:?SUPABASE_SERVICE_ROLE_KEY secret is required}" + + cat < test/containers/.env + COMPOSE_PROJECT_NAME=wb-e2e + CONVERTKIT_HTTP_PORT=9010 + RESEND_HTTP_PORT=9011 + UPSTASH_HTTP_PORT=${UPSTASH_HTTP_PORT:-8079} + UPSTASH_REDIS_PORT=${UPSTASH_REDIS_PORT:-6380} + UPSTASH_TOKEN=${UPSTASH_TOKEN:-local-dev-token} + SUPABASE_PROJECT_NAME=wb-supabase + SUPABASE_HEALTH_TIMEOUT=${SUPABASE_HEALTH_TIMEOUT:-240} + SUPABASE_URL=${SUPABASE_URL:-http://127.0.0.1:54321} + SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY} + EOF + - name: Start mock containers run: npm run containers:up From e5075d3c41c74c5bf53a045dbc14a040627ba872 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Wed, 3 Dec 2025 02:06:17 +0300 Subject: [PATCH 09/49] Update CI workflow files to start dev server and remove build step --- .github/workflows/build-and-test.yml | 53 +- .github/workflows/codeql.yml | 8 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/type-check.yml | 51 -- .vscode/settings.json | 1 + E2E_STESS_TESTS.md | 66 +++ _TODO.md | 1 + package-lock.json | 586 +++++++++---------- package.json | 22 +- test/e2e/specs/07-performance/PERFORMANCE.md | 1 + 10 files changed, 389 insertions(+), 402 deletions(-) delete mode 100644 .github/workflows/type-check.yml create mode 100644 E2E_STESS_TESTS.md diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 02d2e5228..17957c5d1 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,11 +1,13 @@ # Runs build, unit tests, and E2E tests - gates deployment -# @TODO: Push to a preview branch on Vercel, and run the E2E tests on that preview instead of from a dev server -name: CI - Build & Test +name: Build and Test on: push: + branches: + - main pull_request: - branches: [main] + branches: + - main jobs: build-and-test: @@ -39,7 +41,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 @@ -53,7 +55,7 @@ jobs: - name: Sync Astro types run: npm run sync - - name: Run TypeScript check + - name: Run Astro check run: npm run check - name: Run lint @@ -65,7 +67,7 @@ jobs: # Run vitest with coverage. The built-in 'github-actions' reporter # (configured in vitest.config.ts) will create annotations. # Coverage reporters (json-summary, json) are configured in vitest.config.ts. - npx vitest run --coverage + npm run test:coverage - name: Report Coverage uses: davelosert/vitest-coverage-report-action@v2.9.0 @@ -74,11 +76,6 @@ jobs: json-summary-path: './coverage/coverage-summary.json' json-final-path: './coverage/coverage-final.json' - - name: Build project - run: npm run build - env: - NODE_ENV: production - - name: Install Playwright browsers run: npx playwright install --with-deps @@ -120,11 +117,41 @@ jobs: - name: Apply Supabase migrations run: npm run containers:supabase:db-push + - name: Start Astro dev server + run: | + npm run dev -- --host 0.0.0.0 > /tmp/astro-dev.log 2>&1 & + echo $! > /tmp/astro-dev.pid + + - name: Wait for dev server + run: | + for attempt in $(seq 1 60); do + if curl -fsS http://127.0.0.1:4321 >/dev/null; then + echo "✅ Dev server is responding" + exit 0 + fi + sleep 2 + done + echo "❌ Dev server failed to start" >&2 + if [ -f /tmp/astro-dev.log ]; then + echo '--- Astro dev server log ---' + cat /tmp/astro-dev.log + fi + exit 1 + - name: Run Playwright E2E tests run: npx playwright test env: CI: "1" FORCE_COLOR: "1" + E2E_MOCKS: "1" + + - name: Stop Astro dev server + if: always() + run: | + if [ -f /tmp/astro-dev.pid ]; then + kill $(cat /tmp/astro-dev.pid) || true + rm /tmp/astro-dev.pid + fi - name: Upload Playwright report uses: actions/upload-artifact@v4 @@ -159,7 +186,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Deploy to Vercel (Preview) uses: amondnet/vercel-action@v41.1.4 @@ -205,7 +232,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Deploy to Vercel (Production) uses: amondnet/vercel-action@v41.1.4 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b6dd9d244..a789ea188 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -3,9 +3,11 @@ name: CodeQL Advanced Security Scanning on: push: - branches: [main] + branches: + - main pull_request: - branches: [main] + branches: + - main schedule: # Run at 6 AM UTC every Monday - cron: '0 6 * * 1' @@ -26,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 135b2252a..96510a0f6 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Dependency Review uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml deleted file mode 100644 index f13eebac8..000000000 --- a/.github/workflows/type-check.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Fast code quality checks (TypeScript + Linting) -name: Code Quality Check - -on: - push: - pull_request: - branches: [main] - -jobs: - quality-check: - name: Code Quality Check - runs-on: ubuntu-latest - - # Define environment variables once at the job level - # These will be available to ALL steps in this job - # @TODO: Same issue with build-and-test.yml. These are real keys set on GitHub. But the production build only occurs on Vercel, and it's the only place that needs real keys. Once we set up a test framework using Docker containers for Suprabase, Upstash, etc., we should replace these keys with test keys. - env: - CONVERTKIT_API_KEY: ${{ secrets.CONVERTKIT_API_KEY }} - CONVERTKIT_FORM_ID: ${{ secrets.CONVERTKIT_FORM_ID }} - CRON_SECRET: ${{ secrets.CRON_SECRET }} - RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - SUPABASE_URL: ${{ secrets.SUPABASE_URL }} - SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} - SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} - KV_URL: ${{ secrets.KV_URL }} - KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }} - KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }} - KV_REST_API_READ_ONLY_TOKEN: ${{ secrets.KV_REST_API_READ_ONLY_TOKEN }} - REDIS_URL: ${{ secrets.REDIS_URL }} - WEBMENTION_IO_TOKEN: ${{ secrets.WEBMENTION_IO_TOKEN }} - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22.x" - cache: 'npm' - - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Run TypeScript check - run: npm run check - - - name: Run linting - run: npm run lint \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 30f7b3dfa..ab4b772e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ "Bridgy", "bsky", "Btns", + "Buildx", "CASB", "Centricity", "cfduid", diff --git a/E2E_STESS_TESTS.md b/E2E_STESS_TESTS.md new file mode 100644 index 000000000..f0fe52136 --- /dev/null +++ b/E2E_STESS_TESTS.md @@ -0,0 +1,66 @@ + +# E2E Stress Tests + +Our goal is to make the E2E test suite as deterministic as possible, so that we can use it as a gate for CI to make sure that commits and PRs on GitHub are not breaking existing code and can be merged to main. We've spent an entire day running the test cases and fixing errors. Each run, one or more new errors appear, we fix them, and do another run with the same result. The entire test suite seems very flaky. + +## Problems Areas + +### 1. Carousel / Testimonials Hydration Regressions + +- **Symptom:** + +`testimonials.spec.ts` consistently fails in WebKit because the target pagination dot never registers as selected (`Expected: 1 Received: 0`). Earlier stress runs also timed out waiting for `data-carousel-ready`, implying the custom element never finishes initialization. + +- **Diagnostics:** + + - Inspect built HTML for `/testing/carousel` and the home page to confirm the inline ` + + diff --git a/test/e2e/helpers/cookieHelper.ts b/test/e2e/helpers/cookieHelper.ts index a2dd39f32..512926d9a 100644 --- a/test/e2e/helpers/cookieHelper.ts +++ b/test/e2e/helpers/cookieHelper.ts @@ -42,10 +42,9 @@ const createCacheBustingUrl = (url: string, token: string): string => { const captureSnapshot = async (page: Page): Promise => { try { return await page.evaluate(() => { - const entries = performance.getEntriesByType('navigation') as PerformanceEntry[] - const lastEntry = entries[entries.length - 1] as Record | undefined - const isPlaywrightRun = - typeof window !== 'undefined' && (window as Record).isPlaywrightControlled === true + const entries = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[] + const lastEntry = entries[entries.length - 1] + const isPlaywrightRun = typeof window !== 'undefined' && window.isPlaywrightControlled === true return { url: window.location.href, @@ -53,7 +52,7 @@ const captureSnapshot = async (page: Page): Promise => { theme: localStorage.getItem('theme'), transitionPersistCount: document.querySelectorAll('[transition\\:persist]').length, historyLength: history.length, - navigationType: typeof lastEntry?.type === 'string' ? (lastEntry.type as string) : null, + navigationType: typeof lastEntry?.type === 'string' ? lastEntry.type : null, isPlaywrightControlled: isPlaywrightRun, } }) diff --git a/test/e2e/specs/06-accessibility/high-contrast-wcag-compliance.spec.ts b/test/e2e/specs/06-accessibility/high-contrast-wcag-compliance.spec.ts index 3e4d9b748..7530bd7e4 100644 --- a/test/e2e/specs/06-accessibility/high-contrast-wcag-compliance.spec.ts +++ b/test/e2e/specs/06-accessibility/high-contrast-wcag-compliance.spec.ts @@ -116,7 +116,7 @@ test.describe('WCAG Compliance', () => { await page.goto('/') // Zoom in - await page.page.evaluate(() => { + await page.evaluate(() => { document.body.style.zoom = '2' }) @@ -126,7 +126,7 @@ test.describe('WCAG Compliance', () => { await page.expectMainElement() // No horizontal scroll should be needed at 200% zoom (in most cases) - const hasHorizontalScroll = await page.page.evaluate(() => { + const hasHorizontalScroll = await page.evaluate(() => { return document.documentElement.scrollWidth > window.innerWidth }) @@ -172,7 +172,7 @@ test.describe('WCAG Compliance', () => { await page.goto('/') // Check for animations - const animations = await page.page.evaluate(() => { + const animations = await page.evaluate(() => { const elements = document.querySelectorAll('*') const animated = [] @@ -204,7 +204,7 @@ test.describe('WCAG Compliance', () => { await page.goto('/') // Check that animations are disabled/reduced - const hasReducedMotion = await page.page.evaluate(() => { + const hasReducedMotion = await page.evaluate(() => { return window.matchMedia('(prefers-reduced-motion: reduce)').matches }) diff --git a/test/e2e/specs/06-accessibility/keyboard-navigation.spec.ts b/test/e2e/specs/06-accessibility/keyboard-navigation.spec.ts index 9207e8728..c7e2624dd 100644 --- a/test/e2e/specs/06-accessibility/keyboard-navigation.spec.ts +++ b/test/e2e/specs/06-accessibility/keyboard-navigation.spec.ts @@ -24,7 +24,7 @@ test.describe('Keyboard Navigation', () => { for (let i = 0; i < maxTabs; i++) { await page.pressKey('Tab') - const focused = await page.page.evaluate(() => document.activeElement?.tagName) + const focused = await page.evaluate(() => document.activeElement?.tagName) if (focused && ['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT'].includes(focused)) { focusableCount++ } @@ -46,7 +46,7 @@ test.describe('Keyboard Navigation', () => { for (let i = 0; i < 10; i++) { await page.pressKey('Tab') - const pos = await page.page.evaluate(() => { + const pos = await page.evaluate(() => { const el = document.activeElement if (!el) return null const rect = el.getBoundingClientRect() @@ -73,7 +73,7 @@ test.describe('Keyboard Navigation', () => { await page.goto('/contact') // Dismiss cookie modal if it's open (common in test environments) - const consentModalVisible = await page.page.evaluate(() => { + const consentModalVisible = await page.evaluate(() => { const modal = document.getElementById('consent-modal-id') return modal ? window.getComputedStyle(modal).display !== 'none' : false }) @@ -94,7 +94,7 @@ test.describe('Keyboard Navigation', () => { let emailFocused = false for (let i = 0; i < 20; i++) { await page.pressKey('Tab') - const focused = await page.page.evaluate(() => document.activeElement?.getAttribute('type')) + const focused = await page.evaluate(() => document.activeElement?.getAttribute('type')) if (focused === 'email') { emailFocused = true break diff --git a/test/e2e/specs/06-accessibility/wcag-compliance.spec.ts b/test/e2e/specs/06-accessibility/wcag-compliance.spec.ts index 7ec89f230..05760b722 100644 --- a/test/e2e/specs/06-accessibility/wcag-compliance.spec.ts +++ b/test/e2e/specs/06-accessibility/wcag-compliance.spec.ts @@ -114,7 +114,7 @@ test.describe('WCAG Compliance', () => { await page.goto('/') // Zoom in - await page.page.evaluate(() => { + await page.evaluate(() => { document.body.style.zoom = '2' }) @@ -124,7 +124,7 @@ test.describe('WCAG Compliance', () => { await page.expectMainElement() // No horizontal scroll should be needed at 200% zoom (in most cases) - const hasHorizontalScroll = await page.page.evaluate(() => { + const hasHorizontalScroll = await page.evaluate(() => { return document.documentElement.scrollWidth > window.innerWidth }) @@ -170,7 +170,7 @@ test.describe('WCAG Compliance', () => { await page.goto('/') // Check for animations - const animations = await page.page.evaluate(() => { + const animations = await page.evaluate(() => { const elements = document.querySelectorAll('*') const animated = [] @@ -202,7 +202,7 @@ test.describe('WCAG Compliance', () => { await page.goto('/') // Check that animations are disabled/reduced - const hasReducedMotion = await page.page.evaluate(() => { + const hasReducedMotion = await page.evaluate(() => { return window.matchMedia('(prefers-reduced-motion: reduce)').matches }) diff --git a/test/e2e/specs/11-regression/hero-animation-mobile-menu-pause.spec.ts b/test/e2e/specs/11-regression/hero-animation-mobile-menu-pause.spec.ts index 075edc956..ef32486d3 100644 --- a/test/e2e/specs/11-regression/hero-animation-mobile-menu-pause.spec.ts +++ b/test/e2e/specs/11-regression/hero-animation-mobile-menu-pause.spec.ts @@ -108,7 +108,7 @@ test.describe('Hero Animation - Mobile Menu Pause Regression', () => { for (let i = 0; i < 3; i++) { await waitForAnimationFrames(page.page, 12) - const transform = await page.page.evaluate(() => { + const transform = await page.evaluate(() => { const monitorBottom = document.querySelector('.monitorBottom') if (!monitorBottom) return null return window.getComputedStyle(monitorBottom).transform @@ -157,7 +157,7 @@ test.describe('Hero Animation - Mobile Menu Pause Regression', () => { const header = page.locator('#header') // Get initial transform of the splash ::after pseudo-element (should be scale(0)) - const initialTransform = await page.page.evaluate(() => { + const initialTransform = await page.evaluate(() => { const splash = document.querySelector('#mobile-splash') if (!splash) return null const afterStyles = window.getComputedStyle(splash, '::after') @@ -177,7 +177,7 @@ test.describe('Hero Animation - Mobile Menu Pause Regression', () => { await waitForAnimationFrames(page.page, 6) // Check that splash ::after is now scaling up (transform should change) - const expandedTransform = await page.page.evaluate(() => { + const expandedTransform = await page.evaluate(() => { const splash = document.querySelector('#mobile-splash') if (!splash) return null const afterStyles = window.getComputedStyle(splash, '::after') diff --git a/test/e2e/specs/14-system/package-release.spec.ts b/test/e2e/specs/14-system/package-release.spec.ts index 880e43114..373ac0a2b 100644 --- a/test/e2e/specs/14-system/package-release.spec.ts +++ b/test/e2e/specs/14-system/package-release.spec.ts @@ -10,22 +10,45 @@ import { test, expect } from '@test/e2e/helpers' import { BasePage } from '@test/e2e/helpers/pageObjectModels/BasePage' -/* eslint-disable import/no-unresolved */ +type EnvironmentClientValues = { + packageRelease: string + privacyPolicyVersion: string +} + +const environmentClientFixturePath = '/testing/environment-client-values' + +const getEnvironmentClientValues = async (page: BasePage): Promise => { + await page.goto(environmentClientFixturePath, { skipCookieDismiss: true }) + await page.waitForLoadState('networkidle') + await page.waitForFunction(() => { + const values = window.environmentClientValues + if (!values) { + return false + } + return typeof values.packageRelease === 'string' && typeof values.privacyPolicyVersion === 'string' + }) + + return await page.evaluate(() => { + if (!window.environmentClientValues) { + const EvaluationErrorCtor = window.EvaluationError! + throw new EvaluationErrorCtor('environmentClientValues not initialized') + } + return window.environmentClientValues + }) +} + +const getPackageReleaseValue = async (page: BasePage): Promise => { + const values = await getEnvironmentClientValues(page) + return values.packageRelease +} + test.describe('Package Release Integration', () => { test('should expose PACKAGE_RELEASE_VERSION in import.meta.env', async ({ page: playwrightPage, }) => { const page = await BasePage.init(playwrightPage) - await page.goto('/') - await page.waitForLoadState('networkidle') - - // Execute in browser context to check if the env var is available - const packageRelease = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPackageRelease } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPackageRelease() - }) + const packageRelease = await getPackageReleaseValue(page) // Assert that the release is defined expect(packageRelease).toBeDefined() @@ -38,14 +61,7 @@ test.describe('Package Release Integration', () => { test('package release should match package.json format', async ({ page: playwrightPage }) => { const page = await BasePage.init(playwrightPage) - await page.goto('/') - await page.waitForLoadState('networkidle') - - const packageRelease = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPackageRelease } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPackageRelease() - }) + const packageRelease = await getPackageReleaseValue(page) // Split into name and version const [name, version] = packageRelease.split('@') @@ -66,22 +82,12 @@ test.describe('Package Release Integration', () => { // Get release from home page await page.goto('/') await page.waitForLoadState('networkidle') - - const releaseFromHome = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPackageRelease } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPackageRelease() - }) + const releaseFromHome = await getPackageReleaseValue(page) // Get release from privacy page await page.goto('/privacy') await page.waitForLoadState('networkidle') - - const releaseFromPrivacy = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPackageRelease } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPackageRelease() - }) + const releaseFromPrivacy = await getPackageReleaseValue(page) // Both should be identical expect(releaseFromHome).toBe(releaseFromPrivacy) @@ -92,18 +98,9 @@ test.describe('Package Release Integration', () => { }) => { const page = await BasePage.init(playwrightPage) - await page.goto('/') - await page.waitForLoadState('networkidle') - - // Test that it can be imported and used in a module context - const releaseFromModule = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPackageRelease } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPackageRelease() - }) + const releaseFromModule = await getPackageReleaseValue(page) expect(releaseFromModule).toBeDefined() expect(releaseFromModule).toMatch(/^.+@\d+\.\d+\.\d+$/) }) }) -/* eslint-enable import/no-unresolved */ diff --git a/test/e2e/specs/14-system/privacy-policy-version.spec.ts b/test/e2e/specs/14-system/privacy-policy-version.spec.ts index 099444de6..e2fb8a6ee 100644 --- a/test/e2e/specs/14-system/privacy-policy-version.spec.ts +++ b/test/e2e/specs/14-system/privacy-policy-version.spec.ts @@ -10,22 +10,45 @@ import { test, expect } from '@test/e2e/helpers' import { BasePage } from '@test/e2e/helpers/pageObjectModels/BasePage' -/* eslint-disable import/no-unresolved */ +type EnvironmentClientValues = { + packageRelease: string + privacyPolicyVersion: string +} + +const environmentClientFixturePath = '/testing/environment-client-values' + +const getEnvironmentClientValues = async (page: BasePage): Promise => { + await page.goto(environmentClientFixturePath, { skipCookieDismiss: true }) + await page.waitForLoadState('networkidle') + await page.waitForFunction(() => { + const values = window.environmentClientValues + if (!values) { + return false + } + return typeof values.packageRelease === 'string' && typeof values.privacyPolicyVersion === 'string' + }) + + return await page.evaluate(() => { + if (!window.environmentClientValues) { + const EvaluationErrorCtor = window.EvaluationError! + throw new EvaluationErrorCtor('environmentClientValues not initialized') + } + return window.environmentClientValues + }) +} + +const getPrivacyPolicyVersionValue = async (page: BasePage): Promise => { + const values = await getEnvironmentClientValues(page) + return values.privacyPolicyVersion +} + test.describe('Privacy Policy Version Integration', () => { test('should expose PRIVACY_POLICY_VERSION in import.meta.env', async ({ page: playwrightPage, }) => { const page = await BasePage.init(playwrightPage) - await page.goto('/') - await page.waitForLoadState('networkidle') - - // Execute in browser context to check if the env var is available - const privacyPolicyVersion = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPrivacyPolicyVersion } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPrivacyPolicyVersion() - }) + const privacyPolicyVersion = await getPrivacyPolicyVersionValue(page) // Assert that the version is defined expect(privacyPolicyVersion).toBeDefined() @@ -38,14 +61,7 @@ test.describe('Privacy Policy Version Integration', () => { test('privacy policy version should be a valid date', async ({ page: playwrightPage }) => { const page = await BasePage.init(playwrightPage) - await page.goto('/') - await page.waitForLoadState('networkidle') - - const privacyPolicyVersion = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPrivacyPolicyVersion } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPrivacyPolicyVersion() - }) + const privacyPolicyVersion = await getPrivacyPolicyVersionValue(page) // Parse the date to ensure it's valid const parsedDate = new Date(privacyPolicyVersion) @@ -64,22 +80,12 @@ test.describe('Privacy Policy Version Integration', () => { // Get version from home page await page.goto('/') await page.waitForLoadState('networkidle') - - const versionFromHome = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPrivacyPolicyVersion } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPrivacyPolicyVersion() - }) + const versionFromHome = await getPrivacyPolicyVersionValue(page) // Get version from privacy page await page.goto('/privacy') await page.waitForLoadState('networkidle') - - const versionFromPrivacy = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPrivacyPolicyVersion } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPrivacyPolicyVersion() - }) + const versionFromPrivacy = await getPrivacyPolicyVersionValue(page) // Both should be identical expect(versionFromHome).toBe(versionFromPrivacy) @@ -90,18 +96,9 @@ test.describe('Privacy Policy Version Integration', () => { }) => { const page = await BasePage.init(playwrightPage) - await page.goto('/') - await page.waitForLoadState('networkidle') - - // Test that it can be imported and used in a module context - const versionFromModule = await page.page.evaluate(async () => { - // @ts-expect-error - runtime-resolved path - const { getPrivacyPolicyVersion } = await import('/src/components/scripts/utils/environmentClient.ts') - return getPrivacyPolicyVersion() - }) + const versionFromModule = await getPrivacyPolicyVersionValue(page) expect(versionFromModule).toBeDefined() expect(versionFromModule).toMatch(/^\d{4}-\d{2}-\d{2}$/) }) }) -/* eslint-enable import/no-unresolved */ From fdcf6bcb6bb2c4ed1551d5a0677def5951fb4598 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Wed, 3 Dec 2025 04:23:17 +0300 Subject: [PATCH 17/49] Fix E2E stress test errors - lint fixes to package-release and privacy-policy-version --- .../testing/_environment-client-values.ts | 47 ++++++++++++ .../testing/environment-client-values.astro | 76 +++++-------------- test/e2e/helpers/cookieHelper.ts | 4 +- 3 files changed, 66 insertions(+), 61 deletions(-) create mode 100644 src/pages/testing/_environment-client-values.ts diff --git a/src/pages/testing/_environment-client-values.ts b/src/pages/testing/_environment-client-values.ts new file mode 100644 index 000000000..3f4c2e280 --- /dev/null +++ b/src/pages/testing/_environment-client-values.ts @@ -0,0 +1,47 @@ +import { + getPackageRelease, + getPrivacyPolicyVersion, +} from '@components/scripts/utils/environmentClient' + +interface EnvClientValues { + packageRelease: string + privacyPolicyVersion: string +} +type EnvValueKey = 'package-release' | 'privacy-policy-version' +type EnvElementState = 'ready' | 'error' + +const setValue = ( + key: EnvValueKey, + value: string, + state: EnvElementState = 'ready' +) => { + const element: HTMLOutputElement | null = ( + document.querySelector(`[data-env-value="${key}"]`) + ) + if (!element) return + element.textContent = value + element.dataset['state'] = state +} + +const values: EnvClientValues = { + packageRelease: '', + privacyPolicyVersion: '', +} + +try { + values.packageRelease = getPackageRelease() + setValue('package-release', values.packageRelease) +} catch (error) { + values.packageRelease = String(error) + setValue('package-release', values.packageRelease, 'error') +} + +try { + values.privacyPolicyVersion = getPrivacyPolicyVersion() + setValue('privacy-policy-version', values.privacyPolicyVersion) +} catch (error) { + values.privacyPolicyVersion = String(error) + setValue('privacy-policy-version', values.privacyPolicyVersion, 'error') +} + +window.environmentClientValues = values diff --git a/src/pages/testing/environment-client-values.astro b/src/pages/testing/environment-client-values.astro index 4ccb4be1c..2a7ccc7bd 100644 --- a/src/pages/testing/environment-client-values.astro +++ b/src/pages/testing/environment-client-values.astro @@ -1,8 +1,7 @@ --- /** - * @file Environment client fixture for E2E tests. - * @description This page intentionally skips BaseLayout to keep hydration minimal for diagnostics. - * @note Exception: page fixtures normally wrap content in BaseLayout, but this file avoids it on purpose. + * @file Environment client fixture for E2E tests. This page intentionally + * skips BaseLayout to keep hydration minimal for diagnostics. */ const pageTitle = 'Environment Client Values' --- @@ -14,36 +13,42 @@ const pageTitle = 'Environment Client Values'