From bd8629f2440cac9649e7118ec19d64eec3adac51 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 5 May 2026 14:53:43 -0400 Subject: [PATCH 01/38] initial commit for verify --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 4e7120b14..cae9808c5 100644 --- a/readme.md +++ b/readme.md @@ -325,6 +325,7 @@ flowchart BT This section preserves prior setup notes and commands for reference as the repo evolved. + ```bash npm i -D concurrently From 0d3d73550b665c82b020beef2d3762630fc01e13 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 5 May 2026 14:58:54 -0400 Subject: [PATCH 02/38] slight reworks for mock servers, forcing use of cellix packages for some, and using the dev call of the actual servers, like in the case of auth, to further decrease decoupling --- apps/ui-staff/package.json | 2 +- package.json | 2 +- .../community/tasks/create-community.ts | 141 +++++++++++++++--- .../src/shared/support/servers/index.ts | 2 +- .../shared/support/servers/portless-server.ts | 69 +++++++-- .../shared/support/servers/test-api-server.ts | 42 +++--- ...erver.ts => test-community-vite-server.ts} | 12 +- .../support/servers/test-environment.ts | 6 + .../support/servers/test-oauth2-server.ts | 36 +---- .../shared/support/shared-infrastructure.ts | 42 ++++-- .../verification-shared/package.json | 2 +- .../community.page-interface.ts | 2 +- .../src/servers/test-mongodb-server.ts | 93 ++++++------ .../src/settings/local-settings.ts | 2 +- .../src/components/community-create.tsx | 11 +- pnpm-lock.yaml | 6 +- 16 files changed, 302 insertions(+), 168 deletions(-) rename packages/ocom-verification/e2e-tests/src/shared/support/servers/{test-vite-server.ts => test-community-vite-server.ts} (72%) diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index e1a705536..130b030e5 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -9,7 +9,7 @@ "format:check": "biome format .", "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", - "dev": "node ../../build-pipeline/scripts/portless-dev.mjs staff.ownercommunity.localhost vite", + "dev": "pnpm exec portless staff.ownercommunity.localhost --force vite", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/package.json b/package.json index cbf82d3ce..26027ce1f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test:all": "turbo run test:all", "test:coverage": "turbo run test:coverage", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", - "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", + "test:e2e": "pnpm --filter @ocom-verification/e2e-tests test:e2e", "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts index fd4ad2fe8..f24fb78f4 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts @@ -1,9 +1,64 @@ import { CommunityPage, type E2ECommunityPage } from '@ocom-verification/verification-shared/pages'; import { PlaywrightPageAdapter } from '@ocom-verification/verification-shared/pages/playwright'; import { type Actor, Interaction, notes, the } from '@serenity-js/core'; +import type { Response } from 'playwright'; import { BrowseTheWeb } from '../../../shared/abilities/browse-the-web.ts'; import type { CommunityE2ENotes } from '../abilities/community-types.ts'; +const createCommunityOperationName = 'AccountsCommunityCreateContainerCommunityCreate'; +const communityListOperationName = 'AccountsCommunityListContainerCommunitiesForCurrentEndUser'; +const memberListOperationName = 'AccountsCommunityListContainerMembersForCurrentEndUser'; + +type CommunityCreateGraphqlPayload = { + data?: { + communityCreate?: { + status?: { + success?: boolean; + errorMessage?: string | null; + }; + community?: { + name?: string | null; + } | null; + }; + }; + errors?: Array<{ message?: string }>; +}; + +type CommunityListGraphqlPayload = { + data?: { + communitiesForCurrentEndUser?: Array<{ name?: string | null }> | null; + membersForCurrentEndUser?: unknown[] | null; + }; + errors?: Array<{ message?: string }>; +}; + +type GraphqlPayload = { + data?: TData; + errors?: Array<{ message?: string }>; +}; + +const hasGraphqlOperation = (operationName: string) => (response: Response) => { + if (!response.url().includes('/api/graphql') || response.request().method() !== 'POST') { + return false; + } + + return response.request().postData()?.includes(operationName) ?? false; +}; + +const selectGraphqlPayload = (payload: GraphqlPayload | Array> | null, hasExpectedData: (data: TData | undefined) => boolean): GraphqlPayload | null => { + if (!Array.isArray(payload)) { + return payload; + } + + return payload.find((item) => hasExpectedData(item.data)) ?? payload.find((item) => item.errors?.length) ?? null; +}; + +const graphqlErrors = (payload: { errors?: Array<{ message?: string }> } | null): string | undefined => + payload?.errors + ?.map((error) => error.message) + .filter(Boolean) + .join('; '); + /** * Creates a community through the browser UI. */ @@ -11,34 +66,80 @@ export const CreateCommunity = (name: string) => Interaction.where(the`#actor creates community "${name}" via UI`, async (serenityActor) => { const actor = serenityActor as unknown as Actor; const { page } = BrowseTheWeb.withActor(actor); - await page.goto('/community/accounts', { + await page.goto('/community/accounts/create-community', { waitUntil: 'networkidle', }); const adapter = new PlaywrightPageAdapter(page); const communityPage: E2ECommunityPage = new CommunityPage(adapter); - await communityPage.createCommunityButton.click(); await communityPage.fillName(name); + + const createMutationResponse = page.waitForResponse(hasGraphqlOperation(createCommunityOperationName), { timeout: 15_000 }).catch(() => null); + const communityListResponse = page.waitForResponse(hasGraphqlOperation(communityListOperationName), { timeout: 15_000 }).catch(() => null); + const memberListResponse = page.waitForResponse(hasGraphqlOperation(memberListOperationName), { timeout: 15_000 }).catch(() => null); + await communityPage.clickCreate(); - try { - // Wait briefly for validation error to appear (if any) - await communityPage.firstValidationError.waitFor({ state: 'visible', timeout: 500 }).catch(() => { - // No error element appeared, form succeeded - }); - - const isError = await communityPage.firstValidationError.isVisible().catch(() => false); - if (isError) { - const errorText = await communityPage.firstValidationError.textContent(); - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', errorText || 'Validation error')); - return; - } - - // No error found, form submitted successfully - await actor.attemptsTo(notes().set('communityName', name), notes().set('communityCreated', true), notes().set('errorMessage', null)); - } catch { - const errorText = await communityPage.errorToast.textContent(); - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', errorText || 'Unknown error')); + await communityPage.firstValidationError.waitFor({ state: 'visible', timeout: 750 }).catch(() => undefined); + const validationError = await communityPage.firstValidationError.isVisible().catch(() => false); + if (validationError) { + const errorText = await communityPage.firstValidationError.textContent(); + await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', errorText || 'Validation error')); + return; } + + const mutationResponse = await createMutationResponse; + if (!mutationResponse) { + await communityPage.errorToast.waitFor({ state: 'visible', timeout: 1_000 }).catch(() => undefined); + const hasErrorToast = await communityPage.errorToast.isVisible().catch(() => false); + const errorText = hasErrorToast ? await communityPage.errorToast.textContent() : null; + const message = errorText || `No ${createCommunityOperationName} GraphQL response was received`; + await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const payload = selectGraphqlPayload((await mutationResponse.json().catch(() => null)) as CommunityCreateGraphqlPayload | CommunityCreateGraphqlPayload[] | null, (data) => Boolean(data?.communityCreate)); + const graphqlError = graphqlErrors(payload); + const mutationResult = payload?.data?.communityCreate; + const mutationError = mutationResult?.status?.errorMessage ?? graphqlError; + const createdName = mutationResult?.community?.name ?? null; + + if (!mutationResponse.ok || graphqlError || mutationResult?.status?.success !== true || (createdName !== null && createdName !== name)) { + const message = + mutationError || + (mutationResult?.status?.success !== true + ? `${createCommunityOperationName} did not report success: ${JSON.stringify(payload)}` + : createdName !== name + ? `Expected created community name "${name}" but GraphQL returned "${createdName ?? 'null'}"` + : `Community create GraphQL request failed with HTTP ${mutationResponse.status()}`); + await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const listResponse = await communityListResponse; + const listPayload = listResponse + ? selectGraphqlPayload((await listResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.communitiesForCurrentEndUser !== undefined) + : null; + const listGraphqlError = graphqlErrors(listPayload); + const listContainsCreatedCommunity = listPayload?.data?.communitiesForCurrentEndUser?.some((community) => community.name === name) ?? false; + if (!listResponse?.ok() || listGraphqlError || !listContainsCreatedCommunity) { + const message = listGraphqlError || `Expected "${name}" in ${communityListOperationName} response after creation`; + await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const membersResponse = await memberListResponse; + const membersPayload = membersResponse + ? selectGraphqlPayload((await membersResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.membersForCurrentEndUser !== undefined) + : null; + const membersGraphqlError = graphqlErrors(membersPayload); + if (!membersResponse?.ok() || membersGraphqlError) { + const message = membersGraphqlError || `${memberListOperationName} did not complete successfully after creation`; + await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + await page.getByRole('cell', { name, exact: true }).first().waitFor({ state: 'visible', timeout: 5_000 }); + await actor.attemptsTo(notes().set('communityName', name), notes().set('communityCreated', true), notes().set('errorMessage', null)); }); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts index 6f2cca847..8301c3a3f 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts @@ -1,6 +1,6 @@ export { MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; export { PortlessServer } from './portless-server.ts'; export { TestApiServer } from './test-api-server.ts'; +export { TestCommunityViteServer } from './test-community-vite-server.ts'; export { buildUrl, cleanupTestEnvironment, initTestEnvironment, setMongoConnectionString } from './test-environment.ts'; export { TestOAuth2Server } from './test-oauth2-server.ts'; -export { TestViteServer } from './test-vite-server.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index 8b78c1c02..d55edad31 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -1,32 +1,40 @@ import { type ChildProcess, spawn } from 'node:child_process'; -import { getPortlessPath } from './resolve-portless.ts'; /** - * Abstract base class for portless-proxied servers. - * Subclasses define the hostname, command, ready marker, and working directory. - * The base class handles spawning via portless, readiness detection, and shutdown. + * Abstract base class for subprocess-backed test servers. + * Subclasses invoke an app package's own local script directly. */ export abstract class PortlessServer { private process: ChildProcess | null = null; private startedByUs = false; + private readonly useDetachedProcessGroup = process.platform !== 'win32'; protected abstract get probeUrl(): string; protected abstract get readyMarker(): string; protected abstract get serverName(): string; protected abstract get startupTimeoutMs(): number; - protected abstract get spawnArgs(): string[]; protected abstract get cwd(): string; + protected abstract get spawnArgs(): string[]; + protected get executable(): string { + return 'pnpm'; + } + protected get probeRequestInit(): RequestInit { + return {}; + } protected get extraEnv(): Record { return {}; } + protected isProbeHealthy(response: Response): boolean | Promise { + return response.ok; + } async isAlreadyRunning(): Promise { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3_000); - const res = await fetch(this.probeUrl, { signal: controller.signal }); + const res = await fetch(this.probeUrl, { ...this.probeRequestInit, signal: controller.signal }); clearTimeout(timeout); - return res.ok; + return await this.isProbeHealthy(res); } catch { return false; } @@ -43,9 +51,10 @@ export abstract class PortlessServer { // Remove NODE_OPTIONS from child process to avoid tsx import issues delete env['NODE_OPTIONS']; - this.process = spawn(getPortlessPath(), this.spawnArgs, { + this.process = spawn(this.executable, this.spawnArgs, { cwd: this.cwd, env, + detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); this.startedByUs = true; @@ -60,11 +69,11 @@ export abstract class PortlessServer { this.process = null; this.startedByUs = false; - proc.kill('SIGTERM'); + this.killProcess(proc, 'SIGTERM'); await new Promise((resolve) => { const timeout = setTimeout(() => { - proc.kill('SIGKILL'); + this.killProcess(proc, 'SIGKILL'); resolve(); }, 10_000); @@ -92,11 +101,28 @@ export abstract class PortlessServer { }, this.startupTimeoutMs); let stderrOutput = ''; + let ready = false; + + const resolveWhenReachable = () => { + if (ready) { + return; + } + ready = true; + + this.waitForProbeReady() + .then(() => { + clearTimeout(timeout); + resolve(); + }) + .catch((error: unknown) => { + clearTimeout(timeout); + reject(error); + }); + }; proc.stdout?.on('data', (data: Buffer) => { if (data.toString().includes(this.readyMarker)) { - clearTimeout(timeout); - resolve(); + resolveWhenReachable(); } }); @@ -119,4 +145,23 @@ export abstract class PortlessServer { }); }); } + + private async waitForProbeReady(): Promise { + while (!(await this.isAlreadyRunning())) { + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + + private killProcess(proc: ChildProcess, signal: NodeJS.Signals): void { + if (this.useDetachedProcessGroup && proc.pid) { + try { + process.kill(-proc.pid, signal); + return; + } catch { + /* Fall back to killing the direct child below. */ + } + } + + proc.kill(signal); + } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 51657939f..11a02bb50 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -1,28 +1,29 @@ -import { execFileSync } from 'node:child_process'; import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; import { buildUrl, getMongoConnectionString } from './test-environment.ts'; export class TestApiServer extends PortlessServer { - override async start(): Promise { - // Mirror the app's real dev bootstrap so deploy assets and local settings - // stay in sync with recent package-script changes. - const env = { - ...process.env, + protected get probeUrl() { + return buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); + } + protected override get probeRequestInit(): RequestInit { + return { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: '{ __typename }' }), }; - delete env['NODE_OPTIONS']; - - execFileSync('pnpm', ['run', 'predev'], { - cwd: this.cwd, - env, - stdio: 'pipe', - }); - - await super.start(); } + protected override async isProbeHealthy(response: Response): Promise { + if (!response.ok) { + return false; + } - protected get probeUrl() { - return buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); + const payload = (await response.json().catch(() => null)) as { + data?: { __typename?: string }; + errors?: unknown[]; + } | null; + + return payload?.data?.__typename === 'Query' && !payload.errors?.length; } protected get readyMarker() { return 'Functions:'; @@ -34,7 +35,7 @@ export class TestApiServer extends PortlessServer { return 120_000; } protected get spawnArgs() { - return ['data-access.ownercommunity.localhost', 'node', 'start-dev.mjs']; + return ['run', 'dev']; } protected get cwd() { return apiSettings.apiDir; @@ -46,6 +47,11 @@ export class TestApiServer extends PortlessServer { COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), ACCOUNT_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, ACCOUNT_PORTAL_OIDC_ENDPOINT: apiSettings.accountPortalOidcEndpoint, + ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', + STAFF_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, + STAFF_PORTAL_OIDC_ENDPOINT: apiSettings.accountPortalOidcEndpoint, + STAFF_PORTAL_OIDC_AUDIENCE: apiSettings.accountPortalOidcAudience, + STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', VITE_FUNCTION_ENDPOINT: buildUrl('data-access.ownercommunity.localhost', '/api/graphql'), }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts similarity index 72% rename from packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts rename to packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index 133873441..80a5ab82d 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -2,7 +2,11 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; import { buildUrl } from './test-environment.ts'; -export class TestViteServer extends PortlessServer { +/** + * Starts the community (user) portal Vite dev server as a subprocess via `pnpm run dev`. + * This is for the owner-community UI only; a separate server class will be needed for the staff portal. + */ +export class TestCommunityViteServer extends PortlessServer { protected get probeUrl() { return buildUrl('ownercommunity.localhost'); } @@ -10,16 +14,16 @@ export class TestViteServer extends PortlessServer { return 'ready in'; } protected get serverName() { - return 'TestViteServer'; + return 'TestCommunityViteServer'; } protected get startupTimeoutMs() { return 60_000; } protected get spawnArgs() { - return ['ownercommunity.localhost', 'pnpm', 'exec', 'vite']; + return ['run', 'dev']; } protected get cwd() { - return apiSettings.uiDir; + return apiSettings.uiCommunityDir; } protected override get extraEnv() { diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 76c9a1d8d..93e45c951 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -7,6 +7,12 @@ let mongoConnectionString: string | undefined; export function initTestEnvironment() { if (proxyInitialized) return; + // Clean up orphaned route locks from previous runs that crashed or were killed + execFileSync(getPortlessPath(), ['prune'], { + timeout: 10_000, + stdio: 'pipe', + }); + execFileSync(getPortlessPath(), ['proxy', 'start', '-p', '1355'], { timeout: 15_000, stdio: 'pipe', diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index 319f80830..ff76d08dd 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -7,7 +7,7 @@ export class TestOAuth2Server extends PortlessServer { return apiSettings.accountPortalOidcEndpoint; } protected get readyMarker() { - return 'Mock OAuth2 server running'; + return 'Registered OIDC config'; } protected get serverName() { return 'TestOAuth2Server'; @@ -16,44 +16,12 @@ export class TestOAuth2Server extends PortlessServer { return 30_000; } protected get spawnArgs() { - return ['mock-auth.ownercommunity.localhost', 'pnpm', 'exec', 'tsx', 'src/index.ts']; + return ['run', 'dev']; } protected get cwd() { return apiSettings.oauth2MockDir; } - private readonly testUser: { - email: string; - given_name: string; - family_name: string; - }; - - constructor(options?: { - testUser?: { - email?: string; - given_name?: string; - family_name?: string; - }; - }) { - super(); - this.testUser = { - email: options?.testUser?.email ?? 'alice@test.cellix.local', - given_name: options?.testUser?.given_name ?? 'Alice', - family_name: options?.testUser?.family_name ?? 'Test', - }; - } - - protected override get extraEnv() { - return { - EMAIL: this.testUser.email, - GIVEN_NAME: this.testUser.given_name, - FAMILY_NAME: this.testUser.family_name, - BASE_URL: buildUrl('mock-auth.ownercommunity.localhost'), - ALLOWED_REDIRECT_URI: buildUrl('ownercommunity.localhost', '/auth-redirect'), - CLIENT_ID: apiSettings.accountPortalOidcAudience, - }; - } - getUrl(): string { return apiSettings.accountPortalOidcIssuer; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index d3d2c3ece..9541483d4 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,20 +1,20 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; -import { actors } from '@ocom-verification/verification-shared/test-data'; import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestOAuth2Server, TestViteServer } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; let mongoDBServer: MongoDBTestServer | undefined; let oauth2Server: TestOAuth2Server | undefined; let apiServer: TestApiServer | undefined; -let viteServer: TestViteServer | undefined; +let communityViteServer: TestCommunityViteServer | undefined; let apiUrl: string | undefined; let accessToken: string | undefined; let browser: Browser | undefined; let browserBaseUrl: string | undefined; let authenticatedBrowserContext: BrowserContext | undefined; let browseTheWeb: BrowseTheWeb | undefined; +let shutdownHandlersRegistered = false; export interface InfrastructureState { apiUrl: string | undefined; @@ -38,9 +38,9 @@ export async function stopAll(): Promise { await browser.close().catch(() => undefined); browser = undefined; } - if (viteServer) { - await viteServer.stop().catch(() => undefined); - viteServer = undefined; + if (communityViteServer) { + await communityViteServer.stop().catch(() => undefined); + communityViteServer = undefined; } if (apiServer) { await apiServer.stop().catch(() => undefined); @@ -63,15 +63,11 @@ export async function stopAll(): Promise { export async function ensureE2EServers(): Promise { initTestEnvironment(); + registerShutdownHandlers(); + // Phase 1: Start MongoDB and OAuth2 in parallel (no interdependency) mongoDBServer ??= new MongoDBTestServer(); - oauth2Server ??= new TestOAuth2Server({ - testUser: { - email: actors.CommunityOwner.email, - given_name: actors.CommunityOwner.givenName, - family_name: actors.CommunityOwner.familyName, - }, - }); + oauth2Server ??= new TestOAuth2Server(); const mongo = mongoDBServer; const oauth2 = oauth2Server; const phase1: Promise[] = []; @@ -85,9 +81,9 @@ export async function ensureE2EServers(): Promise { // Phase 2: Start API (needs MongoDB conn string), Vite (independent), and generate token (needs OAuth2) in parallel apiServer ??= new TestApiServer(); - viteServer ??= new TestViteServer(); + communityViteServer ??= new TestCommunityViteServer(); const api = apiServer; - const vite = viteServer; + const vite = communityViteServer; const phase2: Promise[] = []; if (!api.isRunning()) { phase2.push( @@ -108,7 +104,7 @@ export async function ensureE2EServers(): Promise { } if (phase2.length > 0) await Promise.all(phase2); - browserBaseUrl = viteServer.getUrl(); + browserBaseUrl = communityViteServer.getUrl(); if (!apiUrl) { apiUrl = apiServer?.getUrl(); @@ -150,3 +146,17 @@ async function ensureAuthenticatedBrowserContext(options: { baseURL?: string; ig throw error; } } + +function registerShutdownHandlers(): void { + if (shutdownHandlersRegistered) return; + shutdownHandlersRegistered = true; + + const shutdown = (signal: string) => { + void stopAll().finally(() => { + process.exit(signal === 'SIGINT' ? 130 : 143); + }); + }; + + process.once('SIGINT', () => shutdown('SIGINT')); + process.once('SIGTERM', () => shutdown('SIGTERM')); +} diff --git a/packages/ocom-verification/verification-shared/package.json b/packages/ocom-verification/verification-shared/package.json index 4f99fb36a..13c3822aa 100644 --- a/packages/ocom-verification/verification-shared/package.json +++ b/packages/ocom-verification/verification-shared/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@apollo/server": "catalog:", + "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", "@ocom/service-mongoose": "workspace:*", "@cucumber/cucumber": "catalog:", "@cucumber/messages": "catalog:", @@ -26,7 +27,6 @@ "graphql-depth-limit": "^1.1.0", "graphql-middleware": "^6.1.35", "mongodb": "catalog:", - "mongodb-memory-server": "^10.2.0", "mongoose": "catalog:" }, "devDependencies": { diff --git a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts index 0a3cc952a..f040c8415 100644 --- a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts +++ b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts @@ -2,4 +2,4 @@ import type { CommunityPage } from '../community.page.ts'; export type UiCommunityPage = Pick; -export type E2ECommunityPage = Pick; +export type E2ECommunityPage = Pick; diff --git a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts index 37937e616..621b4331a 100644 --- a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts +++ b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts @@ -1,11 +1,10 @@ +import { type MongoMemoryReplicaSetDisposer, startMongoMemoryReplicaSet } from '@cellix/server-mongodb-memory-mock-seedwork'; import { ServiceMongoose } from '@ocom/service-mongoose'; import { MongoClient, ObjectId } from 'mongodb'; -import { MongoMemoryReplSet } from 'mongodb-memory-server'; +import { apiSettings } from '../settings/index.ts'; import { getAllMockUsers } from '../test-data/index.ts'; -const MONGO_BINARY_VERSION = '7.0.14'; -const DEFAULT_DB_NAME = 'owner-community-test'; -const MAX_REPLSET_START_ATTEMPTS = 5; +const DEFAULT_REPL_SET_NAME = 'globaldb'; export type MongoDBSeedDataFunction = (connectionString: string, dbName: string) => Promise; @@ -52,28 +51,34 @@ export async function seedOwnerCommunityReferenceData(connectionString: string, } /** - * In-memory MongoDB replica set with a Mongoose service attached. - * Provides the test database for acceptance tests — callers supply - * an optional db name and seed function. + * Test wrapper around the Cellix MongoDB memory mock seedwork. + * The replica set is started by @cellix/server-mongodb-memory-mock-seedwork; this class + * owns readiness checks, test seeding, and the Mongoose service used by tests. */ export class MongoDBTestServer { - private replSet: MongoMemoryReplSet | null = null; + private disposer: MongoMemoryReplicaSetDisposer | null = null; private serviceMongoose: ServiceMongoose | null = null; - private dbName = ''; + private connectionString = ''; + private dbName = apiSettings.cosmosDbName; + private startedByUs = false; async start(options?: MongoDBTestServerStartOptions): Promise { - this.dbName = options?.dbName ?? DEFAULT_DB_NAME; - - const config = { - binary: { version: MONGO_BINARY_VERSION }, - replSet: { name: 'rs0', count: 1, storageEngine: 'wiredTiger' as const }, - ...(options?.port && { instanceOpts: [{ port: options.port }] }), - }; - - this.replSet = await this.createReplicaSetWithRetry(config); - const uri = this.replSet.getUri(); + this.dbName = options?.dbName ?? apiSettings.cosmosDbName; + const port = options?.port ?? apiSettings.cosmosDbPort; + const replSetName = getReplicaSetName(apiSettings.cosmosDbConnectionString) ?? DEFAULT_REPL_SET_NAME; + this.connectionString = buildConnectionString({ port, dbName: this.dbName, replSetName }); + + if (!(await MongoDBTestServer.isReachable(this.connectionString))) { + const { disposer } = await startMongoMemoryReplicaSet({ + port, + dbName: this.dbName, + replSetName, + }); + this.disposer = disposer; + this.startedByUs = true; + } - this.serviceMongoose = new ServiceMongoose(uri, { + this.serviceMongoose = new ServiceMongoose(this.connectionString, { dbName: this.dbName, autoIndex: true, autoCreate: true, @@ -90,7 +95,7 @@ export class MongoDBTestServer { } const seedFn = options?.seedDataFn ?? seedOwnerCommunityReferenceData; - await seedFn(uri, this.dbName); + await seedFn(this.connectionString, this.dbName); } getServiceMongoose(): ServiceMongoose { @@ -101,10 +106,10 @@ export class MongoDBTestServer { } getConnectionString(): string { - if (!this.replSet) { + if (!this.connectionString) { throw new Error('MongoDBTestServer not started'); } - return this.replSet.getUri(); + return this.connectionString; } async stop(): Promise { @@ -112,9 +117,11 @@ export class MongoDBTestServer { await this.serviceMongoose.shutDown(); this.serviceMongoose = null; } - if (this.replSet) { - await this.replSet.stop(); - this.replSet = null; + if (this.disposer && this.startedByUs) { + const disposer = this.disposer; + this.disposer = null; + this.startedByUs = false; + await disposer.stop(); } } @@ -122,31 +129,6 @@ export class MongoDBTestServer { return this.serviceMongoose !== null; } - private async createReplicaSetWithRetry(config: Parameters[0]): Promise { - let lastError: unknown; - - for (let attempt = 1; attempt <= MAX_REPLSET_START_ATTEMPTS; attempt += 1) { - try { - return await MongoMemoryReplSet.create(config); - } catch (error) { - lastError = error; - if (!this.isPortInUseError(error) || attempt === MAX_REPLSET_START_ATTEMPTS || config.instanceOpts) { - throw error; - } - } - } - - throw lastError instanceof Error ? lastError : new Error('Failed to start MongoDB replica set'); - } - - private isPortInUseError(error: unknown): boolean { - if (!(error instanceof Error)) { - return false; - } - - return error.message.includes('already in use') || error.message.includes('EADDRINUSE'); - } - static async isReachable(connectionString: string): Promise { const client = new MongoClient(connectionString, { serverSelectionTimeoutMS: 3_000, @@ -168,3 +150,12 @@ export class MongoDBTestServer { await seedOwnerCommunityReferenceData(connectionString, dbName); } } + +function buildConnectionString(config: { port: number; dbName: string; replSetName: string }): string { + return `mongodb://127.0.0.1:${config.port}/${config.dbName}?replicaSet=${config.replSetName}`; +} + +function getReplicaSetName(connectionString: string): string | undefined { + const match = /[?&]replicaSet=([^&]+)/.exec(connectionString); + return match?.[1] ? decodeURIComponent(match[1]) : undefined; +} diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index 99b4c30f9..cb2be37cc 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -22,7 +22,7 @@ export const apiSettings = { apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), - uiDir: path.dirname(uiEnvPath), + uiCommunityDir: path.dirname(uiEnvPath), } as const; export const uiSettings = { diff --git a/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx b/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx index 11610543b..9d1d703ee 100644 --- a/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx +++ b/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx @@ -4,7 +4,7 @@ import React from 'react'; import type { CommunityCreateInput } from '../generated.tsx'; export interface CommunityCreateProps { - onSave: (values: CommunityCreateInput) => void; + onSave: (values: CommunityCreateInput) => Promise | void; } export const CommunityCreate: React.FC = (props) => { @@ -38,10 +38,13 @@ export const CommunityCreate: React.FC = (props) => {
{ + onFinish={async (values) => { setFormLoading(true); - props.onSave(values); - setFormLoading(false); + try { + await props.onSave(values); + } finally { + setFormLoading(false); + } }} > Date: Wed, 6 May 2026 10:08:10 -0400 Subject: [PATCH 03/38] small changes for coverage and some general suggestions applied, snyk ignore due to type issues --- .snyk | 5 + .../mock-application-services.ts | 2 +- .../src/shared/support/hooks.ts | 4 +- .../acceptance-ui/.c8rc.json | 18 +- .../acceptance-ui/package.json | 10 +- .../community/abilities/community-types.ts | 1 - .../create-community.steps.ts | 157 ------------------ .../create-community.steps.tsx | 123 ++++++++++++++ .../community/step-definitions/index.ts | 2 +- .../community/tasks/create-community.ts | 39 +++-- .../acceptance-ui/src/shared/support/hooks.ts | 14 +- .../src/shared/support/ui/jsdom-setup.ts | 2 + .../src/shared/support/ui/react-render.ts | 32 ++++ .../src/shared/support/ui/react-render.tsx | 13 -- .../acceptance-ui/tsconfig.json | 6 +- .../e2e-tests/src/shared/support/hooks.ts | 4 +- .../shared/support/servers/portless-server.ts | 64 ++++++- .../shared/support/servers/test-api-server.ts | 4 +- .../servers/test-community-vite-server.ts | 8 +- .../support/servers/test-oauth2-server.ts | 8 +- .../src/pages/adapters/jsdom-adapter.ts | 34 +++- .../community.page-interface.ts | 2 +- .../src/servers/graphql-test-server.ts | 43 ++++- .../verification-shared/src/servers/index.ts | 1 + .../src/servers/test-server.interface.ts | 40 +++++ .../verification-shared/src/settings/index.ts | 1 + .../src/settings/timeout-settings.ts | 48 ++++++ pnpm-lock.yaml | 24 ++- 28 files changed, 479 insertions(+), 230 deletions(-) delete mode 100644 packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.ts create mode 100644 packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx create mode 100644 packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts delete mode 100644 packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.tsx create mode 100644 packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts create mode 100644 packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts diff --git a/.snyk b/.snyk index 48d289e07..c1fc54cdf 100644 --- a/.snyk +++ b/.snyk @@ -66,3 +66,8 @@ ignore: reason: 'Apollo usage-reporting-protobuf depends on @apollo/protobufjs and Snyk reports no fixed version. We are accepting this temporarily until Apollo provides a non-vulnerable upgrade path.' expires: '2026-07-31T00:00:00.000Z' created: '2026-04-30T00:00:00.000Z' + 'SNYK-JS-MONGOOSE-16425765': + - '* > mongoose@8.17.0': + reason: 'Temporary ignore while evaluating a safe Mongoose upgrade path; fixed 8.x releases currently fail strict TypeScript declaration checks in this workspace.' + expires: '2026-07-31T00:00:00.000Z' + created: '2026-05-06T00:00:00.000Z' diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts b/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts index 7e774dc2c..ca8096e34 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts @@ -33,7 +33,7 @@ function createNoOpApolloServerService(): ServiceApolloServer; + } as unknown as ServiceApolloServer>; } export function createMockApplicationServicesFactory(serviceMongoose: ServiceMongoose): ApplicationServicesFactory { diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/hooks.ts b/packages/ocom-verification/acceptance-api/src/shared/support/hooks.ts index dc1055837..9e919f49d 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/support/hooks.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/support/hooks.ts @@ -1,11 +1,13 @@ import type { IWorld } from '@cucumber/cucumber'; import { After, AfterAll, Before, setDefaultTimeout } from '@cucumber/cucumber'; +import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { isAgent } from 'std-env'; import { type CellixApiWorld, stopSharedServers } from '../../world.ts'; let printedSuiteHeader = false; -setDefaultTimeout(120_000); +/** Default scenario timeout from centralized configuration */ +setDefaultTimeout(getTimeout('scenario')); Before(async function (this: IWorld) { const world = this as IWorld & CellixApiWorld; diff --git a/packages/ocom-verification/acceptance-ui/.c8rc.json b/packages/ocom-verification/acceptance-ui/.c8rc.json index a76d872cb..0043d5de1 100644 --- a/packages/ocom-verification/acceptance-ui/.c8rc.json +++ b/packages/ocom-verification/acceptance-ui/.c8rc.json @@ -1,7 +1,21 @@ { "all": true, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist"], + "allowExternal": true, + "src": [ + "../../ocom/ui-community-route-accounts/src", + "../../ocom/ui-community-route-admin/src", + "../../ocom/ui-community-route-root/src", + "../../ocom/ui-community-shared/src", + "../../ocom/ui-shared/src", + "../../ocom/ui-staff-route-community-management/src", + "../../ocom/ui-staff-route-finance/src", + "../../ocom/ui-staff-route-root/src", + "../../ocom/ui-staff-route-tech-admin/src", + "../../ocom/ui-staff-route-user-management/src", + "../../ocom/ui-staff-shared/src" + ], + "include": ["**/*.tsx", "**/*.ts"], + "exclude": ["node_modules", "dist", "**/apps/**", "**/ocom-verification/**", "**/generated.*", "**/*.d.ts", "**/*.stories.*", "**/*.test.*", "**/*.spec.*"], "reporter": ["text", "lcovonly"], "report-dir": "coverage" } diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index 77ba7135b..be55622e7 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -9,11 +9,17 @@ "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js" }, "dependencies": { + "@apollo/client": "^3.13.9", "@cucumber/cucumber": "catalog:", + "@dr.pogodin/react-helmet": "^3.0.4", + "@serenity-js/console-reporter": "catalog:", "@serenity-js/core": "catalog:", "@serenity-js/cucumber": "catalog:", - "@serenity-js/console-reporter": "catalog:", "@serenity-js/serenity-bdd": "catalog:", + "antd": "catalog:", + "graphql": "catalog:", + "react": "^19.1.0", + "react-dom": "^19.1.0", "std-env": "^4.0.0" }, "devDependencies": { @@ -25,8 +31,6 @@ "@types/react-dom": "^19.1.6", "c8": "^10.1.3", "jsdom": "^26.1.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", "tsx": "^4.20.3", "typescript": "catalog:" } diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/abilities/community-types.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/abilities/community-types.ts index d38e04c21..c8f9fdf09 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/abilities/community-types.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/abilities/community-types.ts @@ -1,6 +1,5 @@ export interface CommunityUiNotes { communityName: string; - container: HTMLElement; formSubmitted: boolean; lastValidationError: string; } diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.ts deleted file mode 100644 index 3495a9d64..000000000 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { type DataTable, Given, Then, When } from '@cucumber/cucumber'; -import { actors } from '@ocom-verification/verification-shared/test-data'; -import { actorCalled, notes } from '@serenity-js/core'; -import type { CommunityUiNotes } from '../abilities/community-types.ts'; -import { CommunityCreatedFlag } from '../questions/community-created-flag.ts'; -import { CommunityErrorMessage } from '../questions/community-error-message.ts'; -import { CommunityName } from '../questions/community-name.ts'; -import { CreateCommunity } from '../tasks/create-community.ts'; - -// Track last actor used in When steps so Then steps can reference them -let lastActorName = actors.CommunityOwner.name; - -Given('{word} is an authenticated community owner', async (actorName: string) => { - lastActorName = actorName; - const actor = actorCalled(actorName); - - // Set up a minimal form container in jsdom for the test. - const container = document.getElementById('root') ?? document.createElement('div'); - container.innerHTML = ` - - - - - - `; - if (!container.parentElement) { - document.body.appendChild(container); - } - - const form = container.querySelector('form'); - const nameInput = container.querySelector('#community-name'); - - if (!form || !nameInput) { - throw new Error('Community form test fixture did not initialize correctly'); - } - - const syncValidity = () => { - nameInput.setCustomValidity(nameInput.value.trim().length === 0 ? 'Community name cannot be empty' : ''); - }; - - nameInput.addEventListener('input', () => { - syncValidity(); - container.dataset['formSubmitted'] = 'false'; - container.dataset['communityName'] = ''; - container.dataset['lastValidationError'] = ''; - }); - - nameInput.addEventListener('invalid', () => { - container.dataset['formSubmitted'] = 'false'; - container.dataset['communityName'] = ''; - container.dataset['lastValidationError'] = nameInput.validationMessage || 'Community name cannot be empty'; - }); - - form.addEventListener('submit', (event: SubmitEvent) => { - event.preventDefault(); - - const name = nameInput.value; - if (!name || name.trim().length === 0) { - container.dataset['formSubmitted'] = 'false'; - container.dataset['communityName'] = ''; - container.dataset['lastValidationError'] = 'Community name cannot be empty'; - return; - } - - container.dataset['formSubmitted'] = 'true'; - container.dataset['communityName'] = name; - container.dataset['lastValidationError'] = ''; - }); - - syncValidity(); - container.dataset['formSubmitted'] = 'false'; - container.dataset['communityName'] = ''; - container.dataset['lastValidationError'] = ''; - - await actor.attemptsTo(notes().set('container', container)); - await actor.attemptsTo(notes().set('formSubmitted', false)); - await actor.attemptsTo(notes().set('communityName', '')); - await actor.attemptsTo(notes().set('lastValidationError', '')); -}); - -When('{word} creates a community with:', async (actorName: string, dataTable: DataTable) => { - lastActorName = actorName; - const actor = actorCalled(actorName); - const details = dataTable.rowsHash(); - const name = details['name'] ?? ''; - - await actor.attemptsTo(CreateCommunity(name)); -}); - -When('{word} attempts to create a community with:', async (actorName: string, dataTable: DataTable) => { - lastActorName = actorName; - const actor = actorCalled(actorName); - const details = dataTable.rowsHash(); - const name = details['name'] ?? ''; - - await actor.attemptsTo(CreateCommunity(name)); -}); - -Then('the community should be created successfully', async () => { - const actor = actorCalled(lastActorName); - const submitted = await actor.answer(CommunityCreatedFlag()); - - if (!submitted) { - throw new Error('Expected community form to be submitted'); - } -}); - -Then('the community name should be {string}', async (expectedName: string) => { - const actor = actorCalled(lastActorName); - const name = await actor.answer(CommunityName()); - - if (name !== expectedName) { - throw new Error(`Expected community name "${expectedName}" but got "${name}"`); - } -}); - -Then('{word} should see a community error for {string}', async (actorName: string, fieldName: string) => { - const resolvedName = /^(she|he|they)$/i.test(actorName) ? lastActorName : actorName; - const actor = actorCalled(resolvedName); - - let storedError: string | undefined; - try { - storedError = await actor.answer(CommunityErrorMessage()); - } catch { - // No error - } - - if (storedError) { - const lowerError = storedError.toLowerCase(); - const lowerField = fieldName.toLowerCase(); - const isFieldMentioned = lowerError.includes(lowerField); - const isValidationPattern = /cannot be empty|required|missing|invalid|must not be empty/i.test(storedError); - - if (!isFieldMentioned && !isValidationPattern) { - throw new Error(`Expected a validation error related to "${fieldName}", but got: "${storedError}"`); - } - return; - } - - throw new Error(`Expected a validation error for "${fieldName}" but none was found`); -}); - -Then('no community should be created', async () => { - const actor = actorCalled(lastActorName); - - let hasValidationError = false; - try { - const storedError = await actor.answer(CommunityErrorMessage()); - hasValidationError = !!storedError; - } catch { - // No error stored - } - - if (!hasValidationError) { - throw new Error('Expected a validation error to prevent community creation, but no error was captured.'); - } -}); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx new file mode 100644 index 000000000..3f553f1e5 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx @@ -0,0 +1,123 @@ +import { type DataTable, Given, Then, When } from '@cucumber/cucumber'; +import { CommunityPage, type UiCommunityPage } from '@ocom-verification/verification-shared/pages'; +import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; +import { actors } from '@ocom-verification/verification-shared/test-data'; +import { actorCalled, notes } from '@serenity-js/core'; +import { CommunityCreate } from '../../../../../../ocom/ui-community-route-accounts/src/components/community-create.tsx'; +import { mountComponent } from '../../../shared/support/ui/react-render.ts'; +import type { CommunityUiNotes } from '../abilities/community-types.ts'; +import { CommunityCreatedFlag } from '../questions/community-created-flag.ts'; +import { CommunityErrorMessage } from '../questions/community-error-message.ts'; +import { CommunityName } from '../questions/community-name.ts'; +import { CreateCommunity, getContainer, setContainer } from '../tasks/create-community.ts'; + +let lastActorName = actors.CommunityOwner.name; + +Given('{word} is an authenticated community owner', async (actorName: string) => { + lastActorName = actorName; + const actor = actorCalled(actorName); + + const onSave = async (values: { name: string }): Promise => { + await actor.attemptsTo(notes().set('formSubmitted', true), notes().set('communityName', values.name ?? ''), notes().set('lastValidationError', '')); + }; + + const rendered = mountComponent(); + setContainer(rendered.container); + + await actor.attemptsTo(notes().set('formSubmitted', false), notes().set('communityName', ''), notes().set('lastValidationError', '')); +}); + +When('{word} creates a community with:', async (actorName: string, dataTable: DataTable) => { + lastActorName = actorName; + const actor = actorCalled(actorName); + const { name: communityName = '' } = dataTable.rowsHash() as { name?: string }; + + await actor.attemptsTo(CreateCommunity(communityName)); +}); + +When('{word} attempts to create a community with:', async (actorName: string, dataTable: DataTable) => { + lastActorName = actorName; + const actor = actorCalled(actorName); + const { name: communityName = '' } = dataTable.rowsHash() as { name?: string }; + + await actor.attemptsTo(CreateCommunity(communityName)); +}); + +Then('the community should be created successfully', async () => { + const actor = actorCalled(lastActorName); + const submitted = await actor.answer(CommunityCreatedFlag()); + + if (!submitted) { + throw new Error('Expected community form to be submitted'); + } +}); + +Then('the community name should be {string}', async (expectedName: string) => { + const actor = actorCalled(lastActorName); + const name = await actor.answer(CommunityName()); + + if (name !== expectedName) { + throw new Error(`Expected community name "${expectedName}" but got "${name}"`); + } +}); + +Then('{word} should see a community error for {string}', async (actorName: string, fieldName: string) => { + const resolvedName = /^(she|he|they)$/i.test(actorName) ? lastActorName : actorName; + + const container = getContainer(); + const adapter = new JsdomPageAdapter(container); + const page = new CommunityPage(adapter) as UiCommunityPage; + + let storedError: string | undefined; + try { + const errorEl = await page.firstValidationError; + if (errorEl) { + storedError = (await errorEl.textContent()) ?? undefined; + } + } catch { + const actor = actorCalled(resolvedName); + try { + storedError = await actor.answer(CommunityErrorMessage()); + } catch { + // No error found + } + } + + if (storedError) { + const lowerError = storedError.toLowerCase(); + const lowerField = fieldName.toLowerCase(); + const isFieldMentioned = lowerError.includes(lowerField); + const isValidationPattern = /cannot be empty|required|missing|invalid|must not be empty|please input/i.test(storedError); + + if (!isFieldMentioned && !isValidationPattern) { + throw new Error(`Expected a validation error related to "${fieldName}", but got: "${storedError}"`); + } + return; + } + + const errorElements = container.querySelectorAll('.ant-form-item-explain-error'); + if (errorElements.length > 0) { + return; + } + + throw new Error(`Expected a validation error for "${fieldName}" but none was found`); +}); + +Then('no community should be created', async () => { + let hasValidationError = false; + try { + const actor = actorCalled(lastActorName); + const storedError = await actor.answer(CommunityErrorMessage()); + hasValidationError = !!storedError; + } catch { + // No error stored — check DOM + } + + if (!hasValidationError) { + const container = getContainer(); + const errorElements = container.querySelectorAll('.ant-form-item-explain-error'); + if (errorElements.length === 0) { + throw new Error('Expected a validation error to prevent community creation, but no error was found.'); + } + } +}); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/index.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/index.ts index c04c54d61..bc657d814 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/index.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/index.ts @@ -1,2 +1,2 @@ // Community context step definitions -import './create-community.steps.ts'; +import './create-community.steps.tsx'; diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts index abaafabf4..31e419d6b 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts @@ -1,24 +1,37 @@ import { CommunityPage, type UiCommunityPage } from '@ocom-verification/verification-shared/pages'; import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; -import { type Actor, Interaction, notes } from '@serenity-js/core'; -import type { CommunityUiNotes } from '../abilities/community-types.ts'; +import { Interaction } from '@serenity-js/core'; + +let currentContainer: HTMLElement | null = null; + +export function setContainer(container: HTMLElement): void { + currentContainer = container; +} + +export function getContainer(): HTMLElement { + if (!currentContainer) { + throw new Error('No community container available — did the Given step run?'); + } + return currentContainer; +} + +async function flushAsync(): Promise { + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); +} export const CreateCommunity = (name: string) => - Interaction.where(`#actor submits community form with name "${name}"`, async (actor) => { - const container: HTMLElement = await actor.answer(notes().get('container')); + Interaction.where(`#actor fills community name "${name}" and submits`, async () => { + const container = getContainer(); const adapter = new JsdomPageAdapter(container); const page: UiCommunityPage = new CommunityPage(adapter); await page.fillName(name); await page.clickCreate(); - const submitted = container.dataset['formSubmitted'] === 'true'; - const communityName = container.dataset['communityName'] ?? ''; - const lastValidationError = container.dataset['lastValidationError'] ?? ''; - - await (actor as Actor).attemptsTo( - notes().set('formSubmitted', submitted), - notes().set('communityName', communityName), - notes().set('lastValidationError', lastValidationError), - ); + await flushAsync(); }); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/hooks.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/hooks.ts index 394d62a90..212d84a85 100644 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/hooks.ts +++ b/packages/ocom-verification/acceptance-ui/src/shared/support/hooks.ts @@ -1,11 +1,15 @@ -import { After, Before } from '@cucumber/cucumber'; +import { After, Before, setDefaultTimeout } from '@cucumber/cucumber'; +import { getTimeout } from '@ocom-verification/verification-shared/settings'; import type { CellixUiWorld } from '../../world.ts'; -import { unmountComponent } from './ui/react-render.tsx'; +import { unmountComponent } from './ui/react-render.ts'; -Before({ timeout: 30_000 }, async function (this: CellixUiWorld) { +/** Default scenario timeout from centralized configuration */ +setDefaultTimeout(getTimeout('scenario')); + +Before({ timeout: getTimeout('uiInit') }, async function (this: CellixUiWorld) { await this.init(); }); -After({ timeout: 10_000 }, async () => { - await unmountComponent(); +After({ timeout: getTimeout('uiCleanup') }, () => { + unmountComponent(); }); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts index 04ab9e10f..28022a681 100644 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts +++ b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts @@ -42,6 +42,8 @@ safeAssign('HTMLButtonElement', dom.window.HTMLButtonElement); safeAssign('HTMLSelectElement', dom.window.HTMLSelectElement); safeAssign('HTMLAnchorElement', dom.window.HTMLAnchorElement); safeAssign('Element', dom.window.Element); +safeAssign('SVGElement', dom.window.SVGElement); +safeAssign('ShadowRoot', dom.window.ShadowRoot ?? class ShadowRoot {}); safeAssign('Node', dom.window.Node); safeAssign('NodeList', dom.window.NodeList); safeAssign('Event', dom.window.Event); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts new file mode 100644 index 000000000..f8d3f7804 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts @@ -0,0 +1,32 @@ +import { MockedProvider, type MockedResponse } from '@apollo/client/testing'; +import { HelmetProvider } from '@dr.pogodin/react-helmet'; +import { type RenderResult, render } from '@testing-library/react'; +import { App, ConfigProvider } from 'antd'; +import React from 'react'; + +let rendered: RenderResult | null = null; + +export interface MountOptions { + mocks?: MockedResponse[]; + wrapWithRouter?: boolean; +} + +export function mountComponent(ui: React.ReactElement, options?: MountOptions): RenderResult { + unmountComponent(); + + const wrapped = React.createElement(HelmetProvider, null, React.createElement(ConfigProvider, null, React.createElement(App, null, React.createElement(MockedProvider, { mocks: options?.mocks ?? [] }, ui)))); + + rendered = render(wrapped); + return rendered; +} + +export function unmountComponent(): void { + if (rendered) { + rendered.unmount(); + rendered = null; + } +} + +export function getRendered(): RenderResult | null { + return rendered; +} diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.tsx b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.tsx deleted file mode 100644 index 748741500..000000000 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { act } from '@testing-library/react'; - -export async function unmountComponent(): Promise { - const container = document.getElementById('root'); - if (!container) return; - - await act(() => { - container.innerHTML = ''; - delete container.dataset['formSubmitted']; - delete container.dataset['communityName']; - delete container.dataset['lastValidationError']; - }); -} diff --git a/packages/ocom-verification/acceptance-ui/tsconfig.json b/packages/ocom-verification/acceptance-ui/tsconfig.json index 46010ede2..a684e5d7d 100644 --- a/packages/ocom-verification/acceptance-ui/tsconfig.json +++ b/packages/ocom-verification/acceptance-ui/tsconfig.json @@ -5,8 +5,12 @@ "erasableSyntaxOnly": false, "composite": false, "incremental": false, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "noEmit": true, "lib": ["ES2023", "DOM", "DOM.Iterable"], - "rootDir": "./src", + "rootDir": "../..", "outDir": "./dist" }, "include": ["src/**/*.ts", "src/**/*.tsx"] diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 3f08366e1..477877fc7 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -3,12 +3,14 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import type { ITestCaseHookParameter, IWorld } from '@cucumber/cucumber'; import { After, AfterAll, Before, Status, setDefaultTimeout } from '@cucumber/cucumber'; +import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { type CellixE2EWorld, stopSharedServers } from '../../world.ts'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; const currentDir = fileURLToPath(new URL('.', import.meta.url)); -setDefaultTimeout(120_000); +/** Default scenario timeout from centralized configuration */ +setDefaultTimeout(getTimeout('scenario')); Before(async function (this: IWorld) { const world = this as IWorld & CellixE2EWorld; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index d55edad31..814acec20 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -1,10 +1,23 @@ import { type ChildProcess, spawn } from 'node:child_process'; +import type { TestServer } from '@ocom-verification/verification-shared/servers'; +import { getTimeout } from '@ocom-verification/verification-shared/settings'; /** * Abstract base class for subprocess-backed test servers. * Subclasses invoke an app package's own local script directly. + * + * This implements the TestServer interface for consistency with + * GraphQLTestServer (in-process), while providing subprocess isolation + * for full system tests. + * + * Use this for: + * - E2E tests requiring real running servers + * - Full system integration tests + * - Testing the actual build artifacts + * + * For faster API tests, use GraphQLTestServer instead. */ -export abstract class PortlessServer { +export abstract class PortlessServer implements TestServer { private process: ChildProcess | null = null; private startedByUs = false; private readonly useDetachedProcessGroup = process.platform !== 'win32'; @@ -12,26 +25,34 @@ export abstract class PortlessServer { protected abstract get probeUrl(): string; protected abstract get readyMarker(): string; protected abstract get serverName(): string; - protected abstract get startupTimeoutMs(): number; protected abstract get cwd(): string; protected abstract get spawnArgs(): string[]; + protected get executable(): string { return 'pnpm'; } + protected get probeRequestInit(): RequestInit { return {}; } + protected get extraEnv(): Record { return {}; } + protected isProbeHealthy(response: Response): boolean | Promise { return response.ok; } + /** + * Check if server is already running (via health probe). + * Uses centralized health probe timeout. + */ async isAlreadyRunning(): Promise { try { const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 3_000); + const probeTimeout = getTimeout('healthProbe'); + const timeout = setTimeout(() => controller.abort(), probeTimeout); const res = await fetch(this.probeUrl, { ...this.probeRequestInit, signal: controller.signal }); clearTimeout(timeout); return await this.isProbeHealthy(res); @@ -40,6 +61,10 @@ export abstract class PortlessServer { } } + /** + * Start the server subprocess and wait for it to be ready. + * Uses centralized server startup timeout. + */ async start(): Promise { if (this.process || this.startedByUs) return; if (await this.isAlreadyRunning()) return; @@ -62,6 +87,10 @@ export abstract class PortlessServer { await this.waitForReady(); } + /** + * Stop the server gracefully, with fallback to SIGKILL. + * Uses centralized server shutdown timeout. + */ async stop(): Promise { if (!this.process || !this.startedByUs) return; @@ -71,11 +100,12 @@ export abstract class PortlessServer { this.killProcess(proc, 'SIGTERM'); + const shutdownTimeout = getTimeout('serverShutdown'); await new Promise((resolve) => { const timeout = setTimeout(() => { this.killProcess(proc, 'SIGKILL'); resolve(); - }, 10_000); + }, shutdownTimeout); proc.on('exit', () => { clearTimeout(timeout); @@ -84,10 +114,28 @@ export abstract class PortlessServer { }); } + /** + * Check if server is currently running (started by this instance). + */ isRunning(): boolean { return this.process !== null; } + /** + * Get the server URL. + * Subclasses must implement this to return the appropriate URL. + * @throws Error if server is not running + */ + abstract getUrl(): string; + + /** + * Get the startup timeout from centralized configuration. + * Subclasses can override for specific requirements. + */ + protected get startupTimeoutMs(): number { + return getTimeout('serverStartup'); + } + private waitForReady(): Promise { return new Promise((resolve, reject) => { const proc = this.process; @@ -96,9 +144,10 @@ export abstract class PortlessServer { return; } + const startupTimeout = this.startupTimeoutMs; const timeout = setTimeout(() => { - reject(new Error(`${this.serverName} did not start within ${this.startupTimeoutMs}ms`)); - }, this.startupTimeoutMs); + reject(new Error(`${this.serverName} did not start within ${startupTimeout}ms`)); + }, startupTimeout); let stderrOutput = ''; let ready = false; @@ -147,8 +196,9 @@ export abstract class PortlessServer { } private async waitForProbeReady(): Promise { + const probeInterval = getTimeout('healthProbeInterval'); while (!(await this.isAlreadyRunning())) { - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, probeInterval)); } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 11a02bb50..3755cb14e 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -31,9 +31,7 @@ export class TestApiServer extends PortlessServer { protected get serverName() { return 'TestApiServer'; } - protected get startupTimeoutMs() { - return 120_000; - } + protected get spawnArgs() { return ['run', 'dev']; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index 80a5ab82d..a707d7c30 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -16,9 +16,15 @@ export class TestCommunityViteServer extends PortlessServer { protected get serverName() { return 'TestCommunityViteServer'; } - protected get startupTimeoutMs() { + + /** + * Vite typically starts faster than API servers. + * Using a shorter timeout (60s vs default 120s) for faster feedback on failures. + */ + protected override get startupTimeoutMs() { return 60_000; } + protected get spawnArgs() { return ['run', 'dev']; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index ff76d08dd..10c24993c 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -12,9 +12,15 @@ export class TestOAuth2Server extends PortlessServer { protected get serverName() { return 'TestOAuth2Server'; } - protected get startupTimeoutMs() { + + /** + * OAuth2 mock server is lightweight and starts very quickly. + * Using a short timeout (30s vs default 120s) for faster feedback. + */ + protected override get startupTimeoutMs() { return 30_000; } + protected get spawnArgs() { return ['run', 'dev']; } diff --git a/packages/ocom-verification/verification-shared/src/pages/adapters/jsdom-adapter.ts b/packages/ocom-verification/verification-shared/src/pages/adapters/jsdom-adapter.ts index 6fccecbab..06053c39b 100644 --- a/packages/ocom-verification/verification-shared/src/pages/adapters/jsdom-adapter.ts +++ b/packages/ocom-verification/verification-shared/src/pages/adapters/jsdom-adapter.ts @@ -1,4 +1,4 @@ -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import type { ElementHandle, PageAdapter, PageNavigationWaitUntil, PageUrlMatcher } from '../page-adapter.ts'; function getGlobalDocument(container: Element): Document { @@ -30,28 +30,46 @@ class JsdomElementHandle implements ElementHandle { constructor(private readonly el: Element | null) {} fill(value: string): Promise { - if (this.el) { - fireEvent.input(this.el, { target: { value } }); - fireEvent.change(this.el, { target: { value } }); - } + if (!this.el) return Promise.resolve(); + + const input = this.el as HTMLInputElement; + act(() => { + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype, 'value')?.set; + if (nativeInputValueSetter) { + nativeInputValueSetter.call(input, value); + } else { + input.value = value; + } + fireEvent.input(input, { target: { value } }); + fireEvent.change(input, { target: { value } }); + }); return Promise.resolve(); } click(): Promise { if (this.el) { - fireEvent.click(this.el); + const el = this.el; + act(() => { + fireEvent.click(el); + }); } return Promise.resolve(); } check(): Promise { if (this.el instanceof HTMLInputElement) { - fireEvent.click(this.el, { target: { checked: true } }); + const el = this.el; + act(() => { + fireEvent.click(el, { target: { checked: true } }); + }); return Promise.resolve(); } if (this.el) { - fireEvent.click(this.el); + const el = this.el; + act(() => { + fireEvent.click(el); + }); } return Promise.resolve(); } diff --git a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts index f040c8415..584459540 100644 --- a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts +++ b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts @@ -1,5 +1,5 @@ import type { CommunityPage } from '../community.page.ts'; -export type UiCommunityPage = Pick; +export type UiCommunityPage = Pick; export type E2ECommunityPage = Pick; diff --git a/packages/ocom-verification/verification-shared/src/servers/graphql-test-server.ts b/packages/ocom-verification/verification-shared/src/servers/graphql-test-server.ts index 8de3f51e7..acae9ca37 100644 --- a/packages/ocom-verification/verification-shared/src/servers/graphql-test-server.ts +++ b/packages/ocom-verification/verification-shared/src/servers/graphql-test-server.ts @@ -4,6 +4,8 @@ import type { ApplicationServices, ApplicationServicesFactory } from '@ocom/appl import { combinedSchema } from '@ocom/graphql'; import depthLimit from 'graphql-depth-limit'; import { applyMiddleware } from 'graphql-middleware'; +import { getTimeout } from '../settings/index.ts'; +import type { TestServer } from './test-server.interface.ts'; interface GraphContext { applicationServices: ApplicationServices; @@ -11,14 +13,30 @@ interface GraphContext { const MAX_QUERY_DEPTH = 10; -// In-process Apollo Server for API acceptance and integration tests -export class GraphQLTestServer { +/** + * In-process Apollo Server for API acceptance and integration tests. + * + * This server runs the GraphQL schema directly in the test process, + * providing fast feedback with mocked application services. + * + * Use this for: + * - API acceptance tests + * - Unit-like integration tests + * - Fast feedback loops + * + * For full system tests, use PortlessServer-based implementations instead. + */ +export class GraphQLTestServer implements TestServer { private server: ApolloServer | null = null; private url: string | null = null; constructor(private readonly applicationServicesFactory?: ApplicationServicesFactory) {} - async start(port = 0): Promise { + /** + * Start the GraphQL server on the specified port (or random port if 0). + * Uses centralized timeout configuration. + */ + async start(port = 0): Promise { if (this.server) { throw new Error('Test server already started'); } @@ -32,6 +50,9 @@ export class GraphQLTestServer { introspection: false, }); + const timeoutMs = getTimeout('serverStartup'); + const startTime = Date.now(); + const { url } = await startStandaloneServer(this.server, { listen: { port }, context: async ({ req }) => { @@ -49,10 +70,17 @@ export class GraphQLTestServer { }, }); + const elapsed = Date.now() - startTime; + if (elapsed > timeoutMs * 0.8) { + console.warn(`GraphQLTestServer startup took ${elapsed}ms (timeout: ${timeoutMs}ms)`); + } + this.url = url; - return url; } + /** + * Stop the server gracefully. + */ async stop(): Promise { if (!this.server) { return; @@ -63,6 +91,10 @@ export class GraphQLTestServer { this.url = null; } + /** + * Get the server URL. + * @throws Error if server is not running + */ getUrl(): string { if (!this.url) { throw new Error('Test server not started'); @@ -70,6 +102,9 @@ export class GraphQLTestServer { return this.url; } + /** + * Check if server is currently running. + */ isRunning(): boolean { return this.server !== null; } diff --git a/packages/ocom-verification/verification-shared/src/servers/index.ts b/packages/ocom-verification/verification-shared/src/servers/index.ts index 585f779f2..32810914a 100644 --- a/packages/ocom-verification/verification-shared/src/servers/index.ts +++ b/packages/ocom-verification/verification-shared/src/servers/index.ts @@ -7,3 +7,4 @@ export { MongoDBTestServer, seedOwnerCommunityReferenceData, } from './test-mongodb-server.ts'; +export type { TestServer, TestServerOptions } from './test-server.interface.ts'; diff --git a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts new file mode 100644 index 000000000..8b08f6b92 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts @@ -0,0 +1,40 @@ +/** + * Common interface for all test servers (in-process and subprocess). + * + * This abstraction allows acceptance-api and e2e tests to use + * consistent server lifecycle management patterns while choosing + * the appropriate implementation: + * + * - **In-process** (GraphQLTestServer): Fast, isolated, mocked services + * Best for: API acceptance tests, unit-like integration tests + * + * - **Subprocess** (PortlessServer): Full stack, realistic, real services + * Best for: E2E tests, full system integration tests + */ +export interface TestServer { + /** Start the server and return when ready */ + start(): Promise; + + /** Stop the server gracefully */ + stop(): Promise; + + /** Check if server is currently running */ + isRunning(): boolean; + + /** Get the server URL (throws if not running) */ + getUrl(): string; +} + +/** + * Configuration options for test server startup. + */ +export interface TestServerOptions { + /** Port to listen on (0 for random available port) */ + port?: number; + + /** Additional environment variables for subprocess servers */ + env?: Record; + + /** Timeout for server startup (defaults to centralized config) */ + startupTimeoutMs?: number; +} diff --git a/packages/ocom-verification/verification-shared/src/settings/index.ts b/packages/ocom-verification/verification-shared/src/settings/index.ts index 88ed046dd..4e28ad534 100644 --- a/packages/ocom-verification/verification-shared/src/settings/index.ts +++ b/packages/ocom-verification/verification-shared/src/settings/index.ts @@ -7,3 +7,4 @@ export { requireSetting, resolveWorkspacePath, } from './settings-utils.ts'; +export { getTimeout, type TimeoutKey, timeouts } from './timeout-settings.ts'; diff --git a/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts b/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts new file mode 100644 index 000000000..309ef7f3e --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts @@ -0,0 +1,48 @@ +/** + * Centralized timeout configuration for all verification test packages. + * + * These timeouts are intentionally generous to accommodate: + * - CI environments with limited resources + * - First-time server startup (cold starts) + * - Parallel test execution contention + */ +export const timeouts = { + /** Default scenario timeout (2 minutes) */ + scenario: 120_000, + + /** Server startup timeout (2 minutes) */ + serverStartup: 120_000, + + /** Server shutdown graceful period (10 seconds) */ + serverShutdown: 10_000, + + /** Health probe timeout (3 seconds) */ + healthProbe: 3_000, + + /** Health probe retry interval (500ms) */ + healthProbeInterval: 500, + + /** UI initialization timeout (30 seconds) */ + uiInit: 30_000, + + /** UI cleanup timeout (10 seconds) */ + uiCleanup: 10_000, +} as const; + +/** Type for timeout configuration keys */ +export type TimeoutKey = keyof typeof timeouts; + +/** + * Get timeout value with optional override from environment. + * Usage: TIMEOUT_SERVER_STARTUP=300000 npm test + */ +export function getTimeout(key: TimeoutKey): number { + const envOverride = process.env[`TIMEOUT_${key.toUpperCase()}`]; + if (envOverride) { + const parsed = Number.parseInt(envOverride, 10); + if (!Number.isNaN(parsed)) { + return parsed; + } + } + return timeouts[key]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b704dc49..4afa69693 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1045,9 +1045,15 @@ importers: packages/ocom-verification/acceptance-ui: dependencies: + '@apollo/client': + specifier: ^3.13.9 + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cucumber/cucumber': specifier: 'catalog:' version: 12.8.1 + '@dr.pogodin/react-helmet': + specifier: ^3.0.4 + version: 3.0.4(react@19.2.0) '@serenity-js/console-reporter': specifier: 'catalog:' version: 3.42.2 @@ -1060,6 +1066,18 @@ importers: '@serenity-js/serenity-bdd': specifier: 'catalog:' version: 3.42.2 + antd: + specifier: 'catalog:' + version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + graphql: + specifier: 'catalog:' + version: 16.12.0 + react: + specifier: ^19.1.0 + version: 19.2.0 + react-dom: + specifier: ^19.1.0 + version: 19.2.0(react@19.2.0) std-env: specifier: ^4.0.0 version: 4.0.0 @@ -1088,12 +1106,6 @@ importers: jsdom: specifier: ^26.1.0 version: 26.1.0 - react: - specifier: ^19.1.0 - version: 19.2.0 - react-dom: - specifier: ^19.1.0 - version: 19.2.0(react@19.2.0) tsx: specifier: ^4.20.3 version: 4.21.0 From fde5f4d0d4487d1a17ca1da6180f10d1d9c11c61 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 6 May 2026 10:32:00 -0400 Subject: [PATCH 04/38] test coverge and e2e --- build-pipeline/scripts/merge-coverage.js | 190 +++++++++--------- package.json | 4 +- .../acceptance-api/.c8rc.json | 17 +- .../acceptance-api/package.json | 1 + .../acceptance-api/turbo.json | 6 + .../acceptance-ui/.c8rc.json | 28 ++- .../acceptance-ui/package.json | 3 +- .../acceptance-ui/turbo.json | 6 + 8 files changed, 138 insertions(+), 117 deletions(-) diff --git a/build-pipeline/scripts/merge-coverage.js b/build-pipeline/scripts/merge-coverage.js index 349172dd8..81e0a7de8 100755 --- a/build-pipeline/scripts/merge-coverage.js +++ b/build-pipeline/scripts/merge-coverage.js @@ -11,105 +11,103 @@ const __dirname = dirname(__filename); * Simple LCOV merger that combines multiple lcov.info files */ function processLcovContent(content, packagePath) { - const lines = content.split('\n'); - const processedLines = []; - - for (const line of lines) { - if (line.startsWith('SF:')) { - // Extract the file path after 'SF:' - const filePath = line.substring(3); - // Prefix with package path, ensuring no double slashes - const prefixedPath = path.join(packagePath, filePath).replaceAll('\\', '/'); - processedLines.push(`SF:${prefixedPath}`); - } else { - processedLines.push(line); - } - } - - return processedLines.join('\n'); + const lines = content.split('\n'); + const processedLines = []; + + for (const line of lines) { + if (line.startsWith('SF:')) { + // Extract the file path after 'SF:' + const filePath = line.substring(3); + // Prefix with package path, ensuring no double slashes + const prefixedPath = path.join(packagePath, filePath).replaceAll('\\', '/'); + processedLines.push(`SF:${prefixedPath}`); + } else { + processedLines.push(line); + } + } + + return processedLines.join('\n'); } function mergeLcovFiles() { - const rootDir = process.cwd(); - const outputFile = path.join(rootDir, 'coverage', 'lcov.info'); - - // Create output directory - const outputDir = path.dirname(outputFile); - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - // Find all lcov.info files - const lcovFiles = []; - - function findLcovFiles(dir) { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - if (entry.name !== 'node_modules' && entry.name !== '.git') { - findLcovFiles(fullPath); - } - } else if (entry.name === 'lcov.info' && fullPath.includes('/coverage/')) { - lcovFiles.push(fullPath); - } - } - } - - // Search in apps and packages directories - const searchDirs = ['apps', 'packages'].filter(dir => - fs.existsSync(path.join(rootDir, dir)) - ); - - for (const dir of searchDirs) { - findLcovFiles(path.join(rootDir, dir)); - } - - console.log(`Found ${lcovFiles.length} LCOV files:`); - lcovFiles.forEach(file => console.log(` - ${file}`)); - - if (lcovFiles.length === 0) { - console.log('No LCOV files found. Creating empty coverage file.'); - fs.writeFileSync(outputFile, ''); - return; - } - - // Merge all LCOV files - let mergedContent = ''; - - for (const lcovFile of lcovFiles) { - try { - const content = fs.readFileSync(lcovFile, 'utf8'); - if (content.trim()) { - // Compute the package path relative to monorepo root - const packageDir = path.dirname(path.dirname(lcovFile)); // Go up from coverage/ to package/ - const packagePath = path.relative(rootDir, packageDir); - - // Process the LCOV content to prefix SF: paths - const processedContent = processLcovContent(content, packagePath); - - mergedContent += processedContent; - if (!processedContent.endsWith('\n')) { - mergedContent += '\n'; - } - } - } catch (error) { - console.warn(`Warning: Could not read ${lcovFile}: ${error.message}`); - } - } - - // Write merged content - fs.writeFileSync(outputFile, mergedContent); - - console.log(`Merged coverage report written to: ${outputFile}`); - console.log(`Total size: ${mergedContent.length} characters`); - - // Count records - const records = (mergedContent.match(/end_of_record/g) || []).length; - console.log(`Coverage records: ${records}`); + const rootDir = process.cwd(); + const outputFile = path.join(rootDir, 'coverage', 'lcov.info'); + + // Create output directory + const outputDir = path.dirname(outputFile); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Find all lcov.info files + const lcovFiles = []; + + function findLcovFiles(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + if (entry.name !== 'node_modules' && entry.name !== '.git') { + findLcovFiles(fullPath); + } + } else if (entry.name === 'lcov.info' && fullPath.includes('/coverage/')) { + lcovFiles.push(fullPath); + } + } + } + + // Search only in the acceptance/verification packages for coverage + const searchDirs = ['packages/ocom-verification'].filter((dir) => fs.existsSync(path.join(rootDir, dir))); + + for (const dir of searchDirs) { + findLcovFiles(path.join(rootDir, dir)); + } + + console.log(`Found ${lcovFiles.length} LCOV files:`); + lcovFiles.forEach((file) => console.log(` - ${file}`)); + + if (lcovFiles.length === 0) { + console.log('No LCOV files found. Creating empty coverage file.'); + fs.writeFileSync(outputFile, ''); + return; + } + + // Merge all LCOV files + let mergedContent = ''; + + for (const lcovFile of lcovFiles) { + try { + const content = fs.readFileSync(lcovFile, 'utf8'); + if (content.trim()) { + // Compute the package path relative to monorepo root + const packageDir = path.dirname(path.dirname(lcovFile)); // Go up from coverage/ to package/ + const packagePath = path.relative(rootDir, packageDir); + + // Process the LCOV content to prefix SF: paths + const processedContent = processLcovContent(content, packagePath); + + mergedContent += processedContent; + if (!processedContent.endsWith('\n')) { + mergedContent += '\n'; + } + } + } catch (error) { + console.warn(`Warning: Could not read ${lcovFile}: ${error.message}`); + } + } + + // Write merged content + fs.writeFileSync(outputFile, mergedContent); + + console.log(`Merged coverage report written to: ${outputFile}`); + console.log(`Total size: ${mergedContent.length} characters`); + + // Count records + const records = (mergedContent.match(/end_of_record/g) || []).length; + console.log(`Coverage records: ${records}`); } // Run the merger -mergeLcovFiles(); \ No newline at end of file +mergeLcovFiles(); diff --git a/package.json b/package.json index 26027ce1f..7c157ecf5 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "start-emulator:mongo-memory-server": "pnpm --filter @cellix/mock-mongodb-memory-server start", "start-emulator:auth-server": "pnpm --filter @cellix/mock-oauth2-server start", "test:all": "turbo run test:all", - "test:coverage": "turbo run test:coverage", + "test:coverage": "turbo run test:coverage --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "pnpm --filter @ocom-verification/e2e-tests test:e2e", "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", @@ -45,7 +45,7 @@ "sonar:pr": "export PR_NUMBER=$(node build-pipeline/scripts/get-pr-number.cjs) && sonar-scanner -Dsonar.pullrequest.key=$PR_NUMBER -Dsonar.pullrequest.branch=$(git branch --show-current) -Dsonar.pullrequest.base=main", "sonar:pr-windows": "for /f %i in ('node build-pipeline/scripts/get-pr-number.cjs') do set PR_NUMBER=%i && sonar-scanner -Dsonar.pullrequest.key=%PR_NUMBER% -Dsonar.pullrequest.branch=%BRANCH_NAME% -Dsonar.pullrequest.base=main", "check-sonar": "node build-pipeline/scripts/check-sonar-quality-gate.cjs", - "verify": "pnpm run format:check && pnpm run test:coverage:merge && pnpm run knip && pnpm run snyk && pnpm run sonar:pr && pnpm run check-sonar", + "verify": "pnpm run format:check && pnpm run test:coverage:merge && pnpm run test:e2e && pnpm run knip && pnpm run snyk && pnpm run sonar:pr && pnpm run check-sonar", "knip": "knip", "snyk": "pnpm run snyk:test && pnpm run snyk:code", "snyk:report": "pnpm run snyk:monitor && pnpm run snyk:code:report", diff --git a/packages/ocom-verification/acceptance-api/.c8rc.json b/packages/ocom-verification/acceptance-api/.c8rc.json index 429ff417d..64101073f 100644 --- a/packages/ocom-verification/acceptance-api/.c8rc.json +++ b/packages/ocom-verification/acceptance-api/.c8rc.json @@ -1,7 +1,18 @@ { - "all": true, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"], + "allowExternal": true, + "include": [ + "**/ocom/application-services/dist/**", + "**/ocom/context-spec/dist/**", + "**/ocom/domain/dist/**", + "**/ocom/graphql/dist/**", + "**/ocom/graphql-handler/dist/**", + "**/ocom/persistence/dist/**", + "**/ocom/service-apollo-server/dist/**", + "**/ocom/service-mongoose/dist/**", + "**/ocom/service-token-validation/dist/**", + "**/ocom/data-sources-mongoose-models/dist/**" + ], + "exclude": ["**/node_modules/**"], "reporter": ["text", "lcovonly"], "report-dir": "coverage" } diff --git a/packages/ocom-verification/acceptance-api/package.json b/packages/ocom-verification/acceptance-api/package.json index 6e246bde5..6552a999f 100644 --- a/packages/ocom-verification/acceptance-api/package.json +++ b/packages/ocom-verification/acceptance-api/package.json @@ -7,6 +7,7 @@ "scripts": { "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js --format json:./reports/cucumber-report-api.json", "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 --allowExternal -- cucumber-js --format json:./reports/cucumber-report-api.json", + "test:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 -- cucumber-js --format json:./reports/cucumber-report-api.json", "verification:test:coverage:report": "c8 report --allowExternal", "clean": "rimraf dist reports target coverage coverage-c8 coverage-vitest .c8-output" }, diff --git a/packages/ocom-verification/acceptance-api/turbo.json b/packages/ocom-verification/acceptance-api/turbo.json index 93d16a120..addd00035 100644 --- a/packages/ocom-verification/acceptance-api/turbo.json +++ b/packages/ocom-verification/acceptance-api/turbo.json @@ -1,6 +1,12 @@ { "extends": ["//"], "tasks": { + "test:coverage": { + "dependsOn": ["^build"], + "inputs": ["src/**/*.ts", "cucumber.js", "package.json", ".c8rc.json"], + "outputs": ["coverage/**"], + "cache": false + }, "test:acceptance": { "dependsOn": ["^build"], "inputs": ["src/**/*.ts", "cucumber.js", "package.json"], diff --git a/packages/ocom-verification/acceptance-ui/.c8rc.json b/packages/ocom-verification/acceptance-ui/.c8rc.json index 0043d5de1..795f70cde 100644 --- a/packages/ocom-verification/acceptance-ui/.c8rc.json +++ b/packages/ocom-verification/acceptance-ui/.c8rc.json @@ -1,21 +1,19 @@ { - "all": true, "allowExternal": true, - "src": [ - "../../ocom/ui-community-route-accounts/src", - "../../ocom/ui-community-route-admin/src", - "../../ocom/ui-community-route-root/src", - "../../ocom/ui-community-shared/src", - "../../ocom/ui-shared/src", - "../../ocom/ui-staff-route-community-management/src", - "../../ocom/ui-staff-route-finance/src", - "../../ocom/ui-staff-route-root/src", - "../../ocom/ui-staff-route-tech-admin/src", - "../../ocom/ui-staff-route-user-management/src", - "../../ocom/ui-staff-shared/src" + "include": [ + "**/ocom/ui-community-route-accounts/src/**", + "**/ocom/ui-community-route-admin/src/**", + "**/ocom/ui-community-route-root/src/**", + "**/ocom/ui-community-shared/src/**", + "**/ocom/ui-shared/src/**", + "**/ocom/ui-staff-route-community-management/src/**", + "**/ocom/ui-staff-route-finance/src/**", + "**/ocom/ui-staff-route-root/src/**", + "**/ocom/ui-staff-route-tech-admin/src/**", + "**/ocom/ui-staff-route-user-management/src/**", + "**/ocom/ui-staff-shared/src/**" ], - "include": ["**/*.tsx", "**/*.ts"], - "exclude": ["node_modules", "dist", "**/apps/**", "**/ocom-verification/**", "**/generated.*", "**/*.d.ts", "**/*.stories.*", "**/*.test.*", "**/*.spec.*"], + "exclude": ["**/node_modules/**", "**/*.generated.*", "**/*.d.ts", "**/*.stories.*", "**/*.test.*", "**/*.spec.*"], "reporter": ["text", "lcovonly"], "report-dir": "coverage" } diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index be55622e7..ded3f5366 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -6,7 +6,8 @@ "type": "module", "scripts": { "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", - "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js" + "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js", + "test:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js" }, "dependencies": { "@apollo/client": "^3.13.9", diff --git a/packages/ocom-verification/acceptance-ui/turbo.json b/packages/ocom-verification/acceptance-ui/turbo.json index 2aee17196..8d42535cb 100644 --- a/packages/ocom-verification/acceptance-ui/turbo.json +++ b/packages/ocom-verification/acceptance-ui/turbo.json @@ -1,6 +1,12 @@ { "extends": ["//"], "tasks": { + "test:coverage": { + "dependsOn": ["^build"], + "inputs": ["src/**/*.ts", "src/**/*.tsx", "cucumber.js", "package.json", ".c8rc.json"], + "outputs": ["coverage/**"], + "cache": false + }, "test:acceptance": { "dependsOn": ["^build"], "inputs": ["src/**/*.ts", "src/**/*.tsx", "cucumber.js", "package.json"], From 675eeec88551a0e07fa642ca561a981f412b16d8 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 6 May 2026 10:51:37 -0400 Subject: [PATCH 05/38] added pipeline stage for e2e --- build-pipeline/core/monorepo-build-stage.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/build-pipeline/core/monorepo-build-stage.yml b/build-pipeline/core/monorepo-build-stage.yml index 074b76f0c..4c2f53b7e 100644 --- a/build-pipeline/core/monorepo-build-stage.yml +++ b/build-pipeline/core/monorepo-build-stage.yml @@ -263,6 +263,22 @@ stages: ${{ each pair in parameters.buildEnvSettings }}: ${{ pair.key }}: ${{ pair.value }} + # Run E2E tests + - task: Bash@3 + displayName: 'Run E2E tests' + inputs: + targetType: 'inline' + script: | + set -euo pipefail + export NODE_OPTIONS=--max_old_space_size=16384 + export PLAYWRIGHT_BROWSERS_PATH="$(PLAYWRIGHT_BROWSERS_PATH)" + echo "Running E2E tests..." + pnpm run test:e2e + workingDirectory: '' + env: + TURBO_TELEMETRY_DISABLED: 1 + PLAYWRIGHT_BROWSERS_PATH: $(PLAYWRIGHT_BROWSERS_PATH) + # Audit unused dependencies with knip (after packages are built) - task: Bash@3 displayName: 'Audit unused dependencies with knip' From 12b0557eec8563f40fbf9af5f5e169d7f268770f Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 6 May 2026 12:01:53 -0400 Subject: [PATCH 06/38] sourcery feedback changes and coverage script finding swtich --- build-pipeline/core/monorepo-build-stage.yml | 2 +- build-pipeline/scripts/merge-coverage.js | 2 +- package.json | 7 +-- .../acceptance-api/package.json | 2 +- .../acceptance-api/turbo.json | 2 +- .../acceptance-ui/package.json | 2 +- .../create-community.steps.tsx | 44 +++++++++---------- .../community/tasks/create-community.ts | 16 +------ .../src/shared/support/ui/react-render.ts | 1 - .../acceptance-ui/src/world.ts | 21 +++++++++ .../acceptance-ui/turbo.json | 2 +- turbo.json | 7 +++ 12 files changed, 60 insertions(+), 48 deletions(-) diff --git a/build-pipeline/core/monorepo-build-stage.yml b/build-pipeline/core/monorepo-build-stage.yml index 4c2f53b7e..92b45b450 100644 --- a/build-pipeline/core/monorepo-build-stage.yml +++ b/build-pipeline/core/monorepo-build-stage.yml @@ -244,7 +244,7 @@ stages: echo "Testing affected packages only (PR/branch build)..." export TURBO_SCM_BASE="origin/$(System.PullRequest.TargetBranch)" export TURBO_CONCURRENCY=4 - pnpm run test:coverage --affected + pnpm run test:coverage:affected pnpm run merge-lcov-reports else echo "Testing all packages (main branch build)..." diff --git a/build-pipeline/scripts/merge-coverage.js b/build-pipeline/scripts/merge-coverage.js index 81e0a7de8..2aa2454a1 100755 --- a/build-pipeline/scripts/merge-coverage.js +++ b/build-pipeline/scripts/merge-coverage.js @@ -52,7 +52,7 @@ function mergeLcovFiles() { if (entry.name !== 'node_modules' && entry.name !== '.git') { findLcovFiles(fullPath); } - } else if (entry.name === 'lcov.info' && fullPath.includes('/coverage/')) { + } else if (entry.name === 'lcov.info' && fullPath.replaceAll('\\', '/').includes('/coverage/')) { lcovFiles.push(fullPath); } } diff --git a/package.json b/package.json index 7c157ecf5..b13d5af2d 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,11 @@ "start-emulator:mongo-memory-server": "pnpm --filter @cellix/mock-mongodb-memory-server start", "start-emulator:auth-server": "pnpm --filter @cellix/mock-oauth2-server start", "test:all": "turbo run test:all", - "test:coverage": "turbo run test:coverage --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", + "test:coverage": "turbo run test:coverage test:coverage:acceptance", + "test:coverage:affected": "turbo run test:coverage test:coverage:acceptance --affected", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "pnpm --filter @ocom-verification/e2e-tests test:e2e", - "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", + "test:acceptance": "turbo run test:acceptance", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", "test:serenity": "turbo run test:serenity", @@ -46,7 +47,7 @@ "sonar:pr-windows": "for /f %i in ('node build-pipeline/scripts/get-pr-number.cjs') do set PR_NUMBER=%i && sonar-scanner -Dsonar.pullrequest.key=%PR_NUMBER% -Dsonar.pullrequest.branch=%BRANCH_NAME% -Dsonar.pullrequest.base=main", "check-sonar": "node build-pipeline/scripts/check-sonar-quality-gate.cjs", "verify": "pnpm run format:check && pnpm run test:coverage:merge && pnpm run test:e2e && pnpm run knip && pnpm run snyk && pnpm run sonar:pr && pnpm run check-sonar", - "knip": "knip", + "knip": "turbo run build --filter=@cellix/config-vitest && knip", "snyk": "pnpm run snyk:test && pnpm run snyk:code", "snyk:report": "pnpm run snyk:monitor && pnpm run snyk:code:report", "snyk:test": "snyk test --all-projects --org=cellixjs --remote-repo-url=https://github.com/CellixJs/cellixjs --policy-path=.snyk --exclude=dist,build,.turbo,coverage,.agents-work", diff --git a/packages/ocom-verification/acceptance-api/package.json b/packages/ocom-verification/acceptance-api/package.json index 6552a999f..5887447d4 100644 --- a/packages/ocom-verification/acceptance-api/package.json +++ b/packages/ocom-verification/acceptance-api/package.json @@ -7,7 +7,7 @@ "scripts": { "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js --format json:./reports/cucumber-report-api.json", "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 --allowExternal -- cucumber-js --format json:./reports/cucumber-report-api.json", - "test:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 -- cucumber-js --format json:./reports/cucumber-report-api.json", + "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 -- cucumber-js --format json:./reports/cucumber-report-api.json", "verification:test:coverage:report": "c8 report --allowExternal", "clean": "rimraf dist reports target coverage coverage-c8 coverage-vitest .c8-output" }, diff --git a/packages/ocom-verification/acceptance-api/turbo.json b/packages/ocom-verification/acceptance-api/turbo.json index addd00035..bd3626a34 100644 --- a/packages/ocom-verification/acceptance-api/turbo.json +++ b/packages/ocom-verification/acceptance-api/turbo.json @@ -1,7 +1,7 @@ { "extends": ["//"], "tasks": { - "test:coverage": { + "test:coverage:acceptance": { "dependsOn": ["^build"], "inputs": ["src/**/*.ts", "cucumber.js", "package.json", ".c8rc.json"], "outputs": ["coverage/**"], diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index ded3f5366..a3c36c7ee 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -7,7 +7,7 @@ "scripts": { "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js", - "test:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js" + "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js" }, "dependencies": { "@apollo/client": "^3.13.9", diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx index 3f553f1e5..ea8a143d9 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx @@ -1,20 +1,18 @@ import { type DataTable, Given, Then, When } from '@cucumber/cucumber'; import { CommunityPage, type UiCommunityPage } from '@ocom-verification/verification-shared/pages'; import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; -import { actors } from '@ocom-verification/verification-shared/test-data'; import { actorCalled, notes } from '@serenity-js/core'; import { CommunityCreate } from '../../../../../../ocom/ui-community-route-accounts/src/components/community-create.tsx'; import { mountComponent } from '../../../shared/support/ui/react-render.ts'; +import type { CellixUiWorld } from '../../../world.ts'; import type { CommunityUiNotes } from '../abilities/community-types.ts'; import { CommunityCreatedFlag } from '../questions/community-created-flag.ts'; import { CommunityErrorMessage } from '../questions/community-error-message.ts'; import { CommunityName } from '../questions/community-name.ts'; -import { CreateCommunity, getContainer, setContainer } from '../tasks/create-community.ts'; +import { CreateCommunity } from '../tasks/create-community.ts'; -let lastActorName = actors.CommunityOwner.name; - -Given('{word} is an authenticated community owner', async (actorName: string) => { - lastActorName = actorName; +Given('{word} is an authenticated community owner', async function (this: CellixUiWorld, actorName: string) { + this.setCommunityActorName(actorName); const actor = actorCalled(actorName); const onSave = async (values: { name: string }): Promise => { @@ -22,29 +20,29 @@ Given('{word} is an authenticated community owner', async (actorName: string) => }; const rendered = mountComponent(); - setContainer(rendered.container); + this.setCommunityContainer(rendered.container); await actor.attemptsTo(notes().set('formSubmitted', false), notes().set('communityName', ''), notes().set('lastValidationError', '')); }); -When('{word} creates a community with:', async (actorName: string, dataTable: DataTable) => { - lastActorName = actorName; +When('{word} creates a community with:', async function (this: CellixUiWorld, actorName: string, dataTable: DataTable) { + this.setCommunityActorName(actorName); const actor = actorCalled(actorName); const { name: communityName = '' } = dataTable.rowsHash() as { name?: string }; - await actor.attemptsTo(CreateCommunity(communityName)); + await actor.attemptsTo(CreateCommunity(this.getCommunityContainer(), communityName)); }); -When('{word} attempts to create a community with:', async (actorName: string, dataTable: DataTable) => { - lastActorName = actorName; +When('{word} attempts to create a community with:', async function (this: CellixUiWorld, actorName: string, dataTable: DataTable) { + this.setCommunityActorName(actorName); const actor = actorCalled(actorName); const { name: communityName = '' } = dataTable.rowsHash() as { name?: string }; - await actor.attemptsTo(CreateCommunity(communityName)); + await actor.attemptsTo(CreateCommunity(this.getCommunityContainer(), communityName)); }); -Then('the community should be created successfully', async () => { - const actor = actorCalled(lastActorName); +Then('the community should be created successfully', async function (this: CellixUiWorld) { + const actor = actorCalled(this.getCommunityActorName()); const submitted = await actor.answer(CommunityCreatedFlag()); if (!submitted) { @@ -52,8 +50,8 @@ Then('the community should be created successfully', async () => { } }); -Then('the community name should be {string}', async (expectedName: string) => { - const actor = actorCalled(lastActorName); +Then('the community name should be {string}', async function (this: CellixUiWorld, expectedName: string) { + const actor = actorCalled(this.getCommunityActorName()); const name = await actor.answer(CommunityName()); if (name !== expectedName) { @@ -61,10 +59,10 @@ Then('the community name should be {string}', async (expectedName: string) => { } }); -Then('{word} should see a community error for {string}', async (actorName: string, fieldName: string) => { - const resolvedName = /^(she|he|they)$/i.test(actorName) ? lastActorName : actorName; +Then('{word} should see a community error for {string}', async function (this: CellixUiWorld, actorName: string, fieldName: string) { + const resolvedName = /^(she|he|they)$/i.test(actorName) ? this.getCommunityActorName() : actorName; - const container = getContainer(); + const container = this.getCommunityContainer(); const adapter = new JsdomPageAdapter(container); const page = new CommunityPage(adapter) as UiCommunityPage; @@ -103,10 +101,10 @@ Then('{word} should see a community error for {string}', async (actorName: strin throw new Error(`Expected a validation error for "${fieldName}" but none was found`); }); -Then('no community should be created', async () => { +Then('no community should be created', async function (this: CellixUiWorld) { let hasValidationError = false; try { - const actor = actorCalled(lastActorName); + const actor = actorCalled(this.getCommunityActorName()); const storedError = await actor.answer(CommunityErrorMessage()); hasValidationError = !!storedError; } catch { @@ -114,7 +112,7 @@ Then('no community should be created', async () => { } if (!hasValidationError) { - const container = getContainer(); + const container = this.getCommunityContainer(); const errorElements = container.querySelectorAll('.ant-form-item-explain-error'); if (errorElements.length === 0) { throw new Error('Expected a validation error to prevent community creation, but no error was found.'); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts index 31e419d6b..2c2683b3e 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts @@ -2,19 +2,6 @@ import { CommunityPage, type UiCommunityPage } from '@ocom-verification/verifica import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; import { Interaction } from '@serenity-js/core'; -let currentContainer: HTMLElement | null = null; - -export function setContainer(container: HTMLElement): void { - currentContainer = container; -} - -export function getContainer(): HTMLElement { - if (!currentContainer) { - throw new Error('No community container available — did the Given step run?'); - } - return currentContainer; -} - async function flushAsync(): Promise { await new Promise((resolve) => { setTimeout(resolve, 0); @@ -24,9 +11,8 @@ async function flushAsync(): Promise { }); } -export const CreateCommunity = (name: string) => +export const CreateCommunity = (container: HTMLElement, name: string) => Interaction.where(`#actor fills community name "${name}" and submits`, async () => { - const container = getContainer(); const adapter = new JsdomPageAdapter(container); const page: UiCommunityPage = new CommunityPage(adapter); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts index f8d3f7804..d48b7eee8 100644 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts +++ b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts @@ -8,7 +8,6 @@ let rendered: RenderResult | null = null; export interface MountOptions { mocks?: MockedResponse[]; - wrapWithRouter?: boolean; } export function mountComponent(ui: React.ReactElement, options?: MountOptions): RenderResult { diff --git a/packages/ocom-verification/acceptance-ui/src/world.ts b/packages/ocom-verification/acceptance-ui/src/world.ts index 331774a96..6dd725063 100644 --- a/packages/ocom-verification/acceptance-ui/src/world.ts +++ b/packages/ocom-verification/acceptance-ui/src/world.ts @@ -4,12 +4,33 @@ import { CellixUiCast } from './shared/support/cast.ts'; export class CellixUiWorld extends World { private cast!: Cast; + private communityContainer: HTMLElement | null = null; + private communityActorName = ''; init(): Promise { this.cast = new CellixUiCast(); serenity.engage(this.cast); return Promise.resolve(); } + + setCommunityContainer(container: HTMLElement): void { + this.communityContainer = container; + } + + getCommunityContainer(): HTMLElement { + if (!this.communityContainer) { + throw new Error('No community container available — did the Given step run?'); + } + return this.communityContainer; + } + + setCommunityActorName(actorName: string): void { + this.communityActorName = actorName; + } + + getCommunityActorName(): string { + return this.communityActorName; + } } setWorldConstructor(CellixUiWorld); diff --git a/packages/ocom-verification/acceptance-ui/turbo.json b/packages/ocom-verification/acceptance-ui/turbo.json index 8d42535cb..ce064baca 100644 --- a/packages/ocom-verification/acceptance-ui/turbo.json +++ b/packages/ocom-verification/acceptance-ui/turbo.json @@ -1,7 +1,7 @@ { "extends": ["//"], "tasks": { - "test:coverage": { + "test:coverage:acceptance": { "dependsOn": ["^build"], "inputs": ["src/**/*.ts", "src/**/*.tsx", "cucumber.js", "package.json", ".c8rc.json"], "outputs": ["coverage/**"], diff --git a/turbo.json b/turbo.json index e28d37d28..015b635c9 100644 --- a/turbo.json +++ b/turbo.json @@ -73,6 +73,13 @@ "outputs": ["target/**", "reports/**"], "cache": false }, + "test:coverage:acceptance": { + "description": "Runs acceptance test coverage (c8 + Cucumber) for verification packages", + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", "!coverage/**", "!target/**", "!dist/**", "!build/**", "!deploy/**"], + "outputs": ["coverage/**"], + "cache": false + }, "test:serenity": { "description": "Runs SerenityJS end-to-end test suites", "dependsOn": ["^build"], From d222c6be78f0b4e1f066c3bca3a86838130cbfab Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 6 May 2026 12:27:30 -0400 Subject: [PATCH 07/38] local setting load fix for issue --- .../src/settings/local-settings.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index cb2be37cc..7f7a1760e 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -8,6 +8,11 @@ const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); const apiValues = readJsonSettings(apiSettingsPath); const uiValues = readDotEnv(uiEnvPath); +const portlessPort = Number(readSetting(apiValues, 'PORTLESS_PORT', '1355') ?? '1355'); +const oidcHost = readSetting(apiValues, 'OIDC_HOST', 'mock-auth.ownercommunity.localhost') ?? 'mock-auth.ownercommunity.localhost'; + +const oidcBaseUrl = `https://${oidcHost}:${portlessPort}`; + export const apiSettings = { nodeEnv: readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development', isDevelopment: (readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development') === 'development', @@ -16,9 +21,9 @@ export const apiSettings = { cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', cosmosDbPort: Number(readSetting(apiValues, 'COSMOSDB_PORT', '50000')), - accountPortalOidcIssuer: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ISSUER') ?? '', - accountPortalOidcEndpoint: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ENDPOINT') ?? '', - accountPortalOidcAudience: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_AUDIENCE', 'mock-client') ?? '', + accountPortalOidcIssuer: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ISSUER', `${oidcBaseUrl}/community`) ?? `${oidcBaseUrl}/community`, + accountPortalOidcEndpoint: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ENDPOINT', `${oidcBaseUrl}/community/.well-known/jwks.json`) ?? `${oidcBaseUrl}/community/.well-known/jwks.json`, + accountPortalOidcAudience: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_AUDIENCE', 'mock-client') ?? 'mock-client', apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), From d916d5a2c80b7d58fcf1deab3c64616790ee797e Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 6 May 2026 14:04:27 -0400 Subject: [PATCH 08/38] Revert "local setting load fix for issue" This reverts commit d222c6be78f0b4e1f066c3bca3a86838130cbfab. --- .../src/settings/local-settings.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index 7f7a1760e..cb2be37cc 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -8,11 +8,6 @@ const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); const apiValues = readJsonSettings(apiSettingsPath); const uiValues = readDotEnv(uiEnvPath); -const portlessPort = Number(readSetting(apiValues, 'PORTLESS_PORT', '1355') ?? '1355'); -const oidcHost = readSetting(apiValues, 'OIDC_HOST', 'mock-auth.ownercommunity.localhost') ?? 'mock-auth.ownercommunity.localhost'; - -const oidcBaseUrl = `https://${oidcHost}:${portlessPort}`; - export const apiSettings = { nodeEnv: readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development', isDevelopment: (readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development') === 'development', @@ -21,9 +16,9 @@ export const apiSettings = { cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', cosmosDbPort: Number(readSetting(apiValues, 'COSMOSDB_PORT', '50000')), - accountPortalOidcIssuer: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ISSUER', `${oidcBaseUrl}/community`) ?? `${oidcBaseUrl}/community`, - accountPortalOidcEndpoint: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ENDPOINT', `${oidcBaseUrl}/community/.well-known/jwks.json`) ?? `${oidcBaseUrl}/community/.well-known/jwks.json`, - accountPortalOidcAudience: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_AUDIENCE', 'mock-client') ?? 'mock-client', + accountPortalOidcIssuer: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ISSUER') ?? '', + accountPortalOidcEndpoint: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ENDPOINT') ?? '', + accountPortalOidcAudience: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_AUDIENCE', 'mock-client') ?? '', apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), From 9faa245bcf9a08a8a40e4d19a8ccc502a3ec07c1 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 6 May 2026 14:22:31 -0400 Subject: [PATCH 09/38] tried path of least resistance, simply have defaults in local-settings shared --- .../src/settings/local-settings.ts | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index cb2be37cc..416da25ea 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -8,17 +8,36 @@ const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); const apiValues = readJsonSettings(apiSettingsPath); const uiValues = readDotEnv(uiEnvPath); +/** + * Defaults for E2E/acceptance test settings when local.settings.json is absent + * (e.g. CI pipelines). All values are non-secret mock/localhost references used + * exclusively by the test harness — no real credentials are involved. + */ +const ciDefaults = { + COSMOSDB_CONNECTION_STRING: '', + COSMOSDB_DBNAME: 'owner-community', + COSMOSDB_PORT: '50000', + NODE_ENV: 'development', + ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + ACCOUNT_PORTAL_OIDC_ENDPOINT: 'https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json', +} as const; + +function setting(key: keyof typeof ciDefaults): string { + return readSetting(apiValues, key, ciDefaults[key]) ?? ciDefaults[key]; +} + export const apiSettings = { - nodeEnv: readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development', - isDevelopment: (readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development') === 'development', + nodeEnv: setting('NODE_ENV'), + isDevelopment: setting('NODE_ENV') === 'development', - cosmosDbConnectionString: readSetting(apiValues, 'COSMOSDB_CONNECTION_STRING') ?? '', - cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', - cosmosDbPort: Number(readSetting(apiValues, 'COSMOSDB_PORT', '50000')), + cosmosDbConnectionString: setting('COSMOSDB_CONNECTION_STRING'), + cosmosDbName: setting('COSMOSDB_DBNAME'), + cosmosDbPort: Number(setting('COSMOSDB_PORT')), - accountPortalOidcIssuer: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ISSUER') ?? '', - accountPortalOidcEndpoint: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ENDPOINT') ?? '', - accountPortalOidcAudience: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_AUDIENCE', 'mock-client') ?? '', + accountPortalOidcIssuer: setting('ACCOUNT_PORTAL_OIDC_ISSUER'), + accountPortalOidcEndpoint: setting('ACCOUNT_PORTAL_OIDC_ENDPOINT'), + accountPortalOidcAudience: setting('ACCOUNT_PORTAL_OIDC_AUDIENCE'), apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), From 207afa71d8ad1879aa62590022c5394ce89b5b45 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 6 May 2026 15:01:16 -0400 Subject: [PATCH 10/38] another attempt at build pipeline run for portless --- packages/ocom-verification/e2e-tests/package.json | 2 +- .../src/shared/support/servers/test-environment.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index cbe99de57..40e7a7c96 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -5,7 +5,7 @@ "private": true, "type": "module", "scripts": { - "test:e2e": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", + "test:e2e": "pnpm exec portless proxy start -p 1355 && NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", "playwright:install": "playwright install chromium", "clean": "rimraf dist reports target" }, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 93e45c951..3c31f78d9 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -7,17 +7,14 @@ let mongoConnectionString: string | undefined; export function initTestEnvironment() { if (proxyInitialized) return; - // Clean up orphaned route locks from previous runs that crashed or were killed + // Clean up orphaned route locks from previous runs that crashed or were killed. + // The proxy itself is started by the test:e2e script so the portless CA exists + // before Node reads NODE_EXTRA_CA_CERTS at startup. execFileSync(getPortlessPath(), ['prune'], { timeout: 10_000, stdio: 'pipe', }); - execFileSync(getPortlessPath(), ['proxy', 'start', '-p', '1355'], { - timeout: 15_000, - stdio: 'pipe', - }); - proxyInitialized = true; } From 607c718283f35e5e442e0b1cc1e02192c6c0f40b Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 6 May 2026 15:34:23 -0400 Subject: [PATCH 11/38] diagnostics for failure --- .../e2e-tests/src/shared/support/hooks.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 477877fc7..644bc0dce 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -33,6 +33,30 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) await browseTheWeb.page.screenshot({ path: screenshotPath, fullPage: true }); this.attach(fs.readFileSync(screenshotPath), 'image/png'); + + // Diagnostic dump to stdout — visible in CI logs when screenshot artifacts aren't accessible + const { page } = browseTheWeb; + const url = page.url(); + const title = await page.title().catch(() => ''); + const bodyText = await page + .locator('body') + .innerText({ timeout: 1_000 }) + .catch(() => '<body text unavailable>'); + const placeholders = await page + .locator('input, textarea') + .evaluateAll((els: Element[]) => els.map((el) => (el as HTMLInputElement | HTMLTextAreaElement).placeholder || '<no placeholder>')) + .catch(() => [] as string[]); + const headings = await page + .locator('h1, h2, h3') + .allTextContents() + .catch(() => []); + console.log(`\n=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`); + console.log(`URL: ${url}`); + console.log(`Title: ${title}`); + console.log(`Headings: ${JSON.stringify(headings)}`); + console.log(`Input placeholders: ${JSON.stringify(placeholders)}`); + console.log(`Body text (first 500 chars): ${bodyText.slice(0, 500)}`); + console.log(`=== END DIAGNOSTICS ===\n`); } } catch { /* Screenshot capture is best-effort */ From b1ac5315f340fecfad62ee953ee5cf0100c7318c Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Wed, 6 May 2026 16:03:20 -0400 Subject: [PATCH 12/38] more diagnostics --- .../e2e-tests/src/shared/support/hooks.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 644bc0dce..4f94c5a89 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -21,8 +21,9 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) const world = this as IWorld & CellixE2EWorld; if (result?.status === Status.FAILED) { + const browseTheWeb = BrowseTheWeb.current(); + try { - const browseTheWeb = BrowseTheWeb.current(); if (browseTheWeb) { const reportsDir = path.resolve(currentDir, '..', '..', '..', 'reports', 'screenshots'); fs.mkdirSync(reportsDir, { recursive: true }); @@ -33,8 +34,13 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) await browseTheWeb.page.screenshot({ path: screenshotPath, fullPage: true }); this.attach(fs.readFileSync(screenshotPath), 'image/png'); + } + } catch { + /* Screenshot capture is best-effort */ + } - // Diagnostic dump to stdout — visible in CI logs when screenshot artifacts aren't accessible + try { + if (browseTheWeb) { const { page } = browseTheWeb; const url = page.url(); const title = await page.title().catch(() => '<title unavailable>'); @@ -50,16 +56,20 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) .locator('h1, h2, h3') .allTextContents() .catch(() => []); - console.log(`\n=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`); - console.log(`URL: ${url}`); - console.log(`Title: ${title}`); - console.log(`Headings: ${JSON.stringify(headings)}`); - console.log(`Input placeholders: ${JSON.stringify(placeholders)}`); - console.log(`Body text (first 500 chars): ${bodyText.slice(0, 500)}`); - console.log(`=== END DIAGNOSTICS ===\n`); + const diagnostic = [ + `=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`, + `URL: ${url}`, + `Title: ${title}`, + `Headings: ${JSON.stringify(headings)}`, + `Input placeholders: ${JSON.stringify(placeholders)}`, + `Body text (first 500 chars): ${bodyText.slice(0, 500)}`, + '=== END DIAGNOSTICS ===', + ].join('\n'); + this.attach(diagnostic, 'text/plain'); + process.stderr.write(`\n${diagnostic}\n`); } } catch { - /* Screenshot capture is best-effort */ + /* Diagnostic capture is best-effort */ } } From 75edcebfcbbddd4ffa0bf6d1e07170fea3b9f35d Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Wed, 6 May 2026 16:44:21 -0400 Subject: [PATCH 13/38] last diagnostics attempt --- .../e2e-tests/src/shared/support/hooks.ts | 34 +++++++++++++++++++ .../shared/support/shared-infrastructure.ts | 2 ++ 2 files changed, 36 insertions(+) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 4f94c5a89..6138105e1 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -7,6 +7,27 @@ import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { type CellixE2EWorld, stopSharedServers } from '../../world.ts'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; +type PlaywrightPage = BrowseTheWeb['page']; + +const consoleMessagesByPage = new WeakMap<PlaywrightPage, string[]>(); + +type ConsoleMessageLike = { type(): string; text(): string }; +type RequestLike = { method(): string; url(): string; failure(): { errorText: string } | null }; + +export function attachConsoleCapture(page: PlaywrightPage): void { + const buffer: string[] = []; + consoleMessagesByPage.set(page, buffer); + page.on('console', (msg: ConsoleMessageLike) => { + buffer.push(`[${msg.type()}] ${msg.text()}`); + }); + page.on('pageerror', (err: Error) => { + buffer.push(`[pageerror] ${err.message}`); + }); + page.on('requestfailed', (req: RequestLike) => { + buffer.push(`[requestfailed] ${req.method()} ${req.url()} — ${req.failure()?.errorText ?? 'unknown'}`); + }); +} + const currentDir = fileURLToPath(new URL('.', import.meta.url)); /** Default scenario timeout from centralized configuration */ @@ -44,6 +65,14 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) const { page } = browseTheWeb; const url = page.url(); const title = await page.title().catch(() => '<title unavailable>'); + const rootHtml = await page + .locator('#root') + .innerHTML({ timeout: 1_000 }) + .catch(() => '<#root unavailable>'); + const scriptSrcs = await page + .locator('script[src]') + .evaluateAll((els: Element[]) => els.map((el) => (el as HTMLScriptElement).src)) + .catch(() => [] as string[]); const bodyText = await page .locator('body') .innerText({ timeout: 1_000 }) @@ -56,13 +85,18 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) .locator('h1, h2, h3') .allTextContents() .catch(() => []); + const consoleLog = consoleMessagesByPage.get(page) ?? []; const diagnostic = [ `=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`, `URL: ${url}`, `Title: ${title}`, + `Script tags: ${JSON.stringify(scriptSrcs)}`, + `#root innerHTML (first 1000 chars): ${rootHtml.slice(0, 1000)}`, `Headings: ${JSON.stringify(headings)}`, `Input placeholders: ${JSON.stringify(placeholders)}`, `Body text (first 500 chars): ${bodyText.slice(0, 500)}`, + `Browser console (last 30 messages):`, + ...consoleLog.slice(-30), '=== END DIAGNOSTICS ===', ].join('\n'); this.attach(diagnostic, 'text/plain'); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index 9541483d4..6eb4dcdae 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,6 +1,7 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; +import { attachConsoleCapture } from './hooks.ts'; import { performOAuth2Login } from './oauth2-login.ts'; import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; @@ -134,6 +135,7 @@ async function ensureAuthenticatedBrowserContext(options: { baseURL?: string; ig } const seedPage = await authenticatedBrowserContext.newPage(); + attachConsoleCapture(seedPage); try { if (options.performLogin) { From e114564704707e48882ddb9057dcababcce0d05f Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 09:04:29 -0400 Subject: [PATCH 14/38] pinning dev mod to see if this resolves issue --- .../src/shared/support/servers/test-community-vite-server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index a707d7c30..10f6cd0f7 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -38,6 +38,7 @@ export class TestCommunityViteServer extends PortlessServer { return { BROWSER: 'none', + NODE_ENV: 'development', VITE_BASE_URL: uiBase, VITE_AAD_B2C_ACCOUNT_AUTHORITY: apiSettings.accountPortalOidcIssuer, VITE_AAD_B2C_REDIRECT_URI: `${uiBase}/auth-redirect`, From f7fc004843780c780fdb03cecbee034748f3df27 Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 09:26:14 -0400 Subject: [PATCH 15/38] another test run --- apps/api/start-dev.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 7c705d8c7..edcb6a350 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -18,7 +18,10 @@ const childEnv = { NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(), }; -const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort], { +// `--cors '*'` matches Host.CORS in local.settings.json but does not depend on +// that file existing — local.settings.json is gitignored, so CI has no CORS +// allowance otherwise and the UI's cross-origin GraphQL requests are blocked. +const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], { stdio: 'inherit', env: childEnv, }); From b95369e69b21dff84aaf996c4267fe3857744adc Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 09:50:08 -0400 Subject: [PATCH 16/38] more diagnostics --- .../e2e-tests/src/shared/support/hooks.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 6138105e1..6647de6b8 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -13,6 +13,7 @@ const consoleMessagesByPage = new WeakMap<PlaywrightPage, string[]>(); type ConsoleMessageLike = { type(): string; text(): string }; type RequestLike = { method(): string; url(): string; failure(): { errorText: string } | null }; +type ResponseLike = { status(): number; url(): string; request(): { method(): string } }; export function attachConsoleCapture(page: PlaywrightPage): void { const buffer: string[] = []; @@ -26,6 +27,12 @@ export function attachConsoleCapture(page: PlaywrightPage): void { page.on('requestfailed', (req: RequestLike) => { buffer.push(`[requestfailed] ${req.method()} ${req.url()} — ${req.failure()?.errorText ?? 'unknown'}`); }); + page.on('response', (res: ResponseLike) => { + const status = res.status(); + if (status >= 400) { + buffer.push(`[response ${status}] ${res.request().method()} ${res.url()}`); + } + }); } const currentDir = fileURLToPath(new URL('.', import.meta.url)); From 597cb6c6192cacf5b0006c36e6a1e1594ece25e4 Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 10:27:45 -0400 Subject: [PATCH 17/38] more diagnostics... --- .../e2e-tests/src/shared/support/hooks.ts | 66 +++++++++++++++++-- .../shared/support/servers/portless-server.ts | 42 ++++++++++-- .../shared/support/shared-infrastructure.ts | 25 +++++++ 3 files changed, 121 insertions(+), 12 deletions(-) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 6647de6b8..52f9406e4 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -6,14 +6,33 @@ import { After, AfterAll, Before, Status, setDefaultTimeout } from '@cucumber/cu import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { type CellixE2EWorld, stopSharedServers } from '../../world.ts'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; +import { getServerDiagnostics, probeApiHealth } from './shared-infrastructure.ts'; type PlaywrightPage = BrowseTheWeb['page']; const consoleMessagesByPage = new WeakMap<PlaywrightPage, string[]>(); +function indent(text: string, prefix: string): string { + return text + .split('\n') + .map((line) => prefix + line) + .join('\n'); +} + type ConsoleMessageLike = { type(): string; text(): string }; -type RequestLike = { method(): string; url(): string; failure(): { errorText: string } | null }; -type ResponseLike = { status(): number; url(): string; request(): { method(): string } }; +type RequestLike = { + method(): string; + url(): string; + failure(): { errorText: string } | null; + headers(): Record<string, string>; + postData(): string | null; +}; +type ResponseLike = { + status(): number; + url(): string; + request(): RequestLike; + text(): Promise<string>; +}; export function attachConsoleCapture(page: PlaywrightPage): void { const buffer: string[] = []; @@ -27,11 +46,32 @@ export function attachConsoleCapture(page: PlaywrightPage): void { page.on('requestfailed', (req: RequestLike) => { buffer.push(`[requestfailed] ${req.method()} ${req.url()} — ${req.failure()?.errorText ?? 'unknown'}`); }); - page.on('response', (res: ResponseLike) => { + page.on('response', async (res: ResponseLike) => { const status = res.status(); - if (status >= 400) { - buffer.push(`[response ${status}] ${res.request().method()} ${res.url()}`); + if (status < 400) return; + const req = res.request(); + const url = res.url(); + // Skip noisy static-asset 404s (favicons, source maps, vite client probes) + // to keep the diagnostic focused on API/data calls. + const isStaticNoise = /\.(ico|png|svg|map|webp|woff2?)(\?|$)/i.test(url); + if (isStaticNoise) { + buffer.push(`[response ${status}] ${req.method()} ${url}`); + return; + } + let body = ''; + try { + body = (await res.text()).slice(0, 500); + } catch (err) { + body = `<body unavailable: ${err instanceof Error ? err.message : 'unknown'}>`; } + const reqBody = req.postData()?.slice(0, 300) ?? ''; + const reqHeaders = req.headers(); + const interestingHeaders = ['authorization', 'content-type', 'x-community-id', 'x-member-id', 'apollographql-client-name']; + const headerSummary = interestingHeaders + .map((h) => (reqHeaders[h] ? `${h}=${h === 'authorization' ? `${reqHeaders[h].slice(0, 20)}…(len=${reqHeaders[h].length})` : reqHeaders[h]}` : null)) + .filter(Boolean) + .join(', '); + buffer.push(`[response ${status}] ${req.method()} ${url}\n req-headers: ${headerSummary}\n req-body: ${reqBody}\n res-body: ${body}`); }); } @@ -93,6 +133,16 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) .allTextContents() .catch(() => []); const consoleLog = consoleMessagesByPage.get(page) ?? []; + const apiHealth = await probeApiHealth(); + const serverDiagnostics = getServerDiagnostics(); + const serverSummary = serverDiagnostics + .map((s) => { + const exitDesc = s.exitInfo ? `EXITED code=${s.exitInfo.code} signal=${s.exitInfo.signal}` : s.alive ? `alive pid=${s.pid}` : 'no process'; + const stderrTail = s.stderrTail.trim() ? `\n stderr-tail:\n${indent(s.stderrTail.slice(-2000), ' ')}` : ''; + const stdoutTail = s.stdoutTail.trim() ? `\n stdout-tail:\n${indent(s.stdoutTail.slice(-1500), ' ')}` : ''; + return ` - ${s.name}: ${exitDesc}${stderrTail}${stdoutTail}`; + }) + .join('\n'); const diagnostic = [ `=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`, `URL: ${url}`, @@ -102,8 +152,10 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) `Headings: ${JSON.stringify(headings)}`, `Input placeholders: ${JSON.stringify(placeholders)}`, `Body text (first 500 chars): ${bodyText.slice(0, 500)}`, - `Browser console (last 30 messages):`, - ...consoleLog.slice(-30), + `API health probe at failure: ${JSON.stringify(apiHealth)}`, + `Subprocess diagnostics:\n${serverSummary}`, + `Browser console (last 50 messages):`, + ...consoleLog.slice(-50), '=== END DIAGNOSTICS ===', ].join('\n'); this.attach(diagnostic, 'text/plain'); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index 814acec20..0092a91bd 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -17,10 +17,20 @@ import { getTimeout } from '@ocom-verification/verification-shared/settings'; * * For faster API tests, use GraphQLTestServer instead. */ +const LOG_TAIL_BYTES = 16_384; + +function appendCapped(existing: string, chunk: string): string { + const combined = existing + chunk; + return combined.length > LOG_TAIL_BYTES ? combined.slice(-LOG_TAIL_BYTES) : combined; +} + export abstract class PortlessServer implements TestServer { private process: ChildProcess | null = null; private startedByUs = false; private readonly useDetachedProcessGroup = process.platform !== 'win32'; + private capturedStdout = ''; + private capturedStderr = ''; + private exitInfo: { code: number | null; signal: NodeJS.Signals | null } | null = null; protected abstract get probeUrl(): string; protected abstract get readyMarker(): string; @@ -136,6 +146,21 @@ export abstract class PortlessServer implements TestServer { return getTimeout('serverStartup'); } + /** + * Diagnostic snapshot for failure reporting. Includes whether the underlying + * process is still alive and the most recent stdout/stderr captured. + */ + getDiagnostics(): { name: string; alive: boolean; pid: number | undefined; exitInfo: { code: number | null; signal: NodeJS.Signals | null } | null; stdoutTail: string; stderrTail: string } { + return { + name: this.serverName, + alive: this.process !== null && this.exitInfo === null, + pid: this.process?.pid, + exitInfo: this.exitInfo, + stdoutTail: this.capturedStdout, + stderrTail: this.capturedStderr, + }; + } + private waitForReady(): Promise<void> { return new Promise((resolve, reject) => { const proc = this.process; @@ -149,7 +174,6 @@ export abstract class PortlessServer implements TestServer { reject(new Error(`${this.serverName} did not start within ${startupTimeout}ms`)); }, startupTimeout); - let stderrOutput = ''; let ready = false; const resolveWhenReachable = () => { @@ -169,14 +193,18 @@ export abstract class PortlessServer implements TestServer { }); }; + // stdout/stderr listeners persist beyond startup so we keep collecting + // logs for post-failure diagnostics. Buffers are size-capped. proc.stdout?.on('data', (data: Buffer) => { - if (data.toString().includes(this.readyMarker)) { + const text = data.toString(); + this.capturedStdout = appendCapped(this.capturedStdout, text); + if (text.includes(this.readyMarker)) { resolveWhenReachable(); } }); proc.stderr?.on('data', (data: Buffer) => { - stderrOutput += data.toString(); + this.capturedStderr = appendCapped(this.capturedStderr, data.toString()); }); proc.on('error', (err) => { @@ -186,11 +214,15 @@ export abstract class PortlessServer implements TestServer { reject(new Error(`${this.serverName} failed to start: ${err.message}`)); }); - proc.on('exit', (code) => { + proc.on('exit', (code, signal) => { clearTimeout(timeout); + this.exitInfo = { code, signal }; this.process = null; this.startedByUs = false; - reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}). stderr: ${stderrOutput.slice(-2000)}`)); + // Reject is a no-op once the promise has settled, but the exitInfo + // + log buffers above let the After hook surface a post-startup + // crash on the next failed scenario. + reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}). stderr: ${this.capturedStderr.slice(-2000)}`)); }); }); } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index 6eb4dcdae..87e8dc0ac 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -27,6 +27,31 @@ export function getState(): InfrastructureState { return { apiUrl, accessToken, browseTheWeb }; } +export function getServerDiagnostics() { + const servers = [mongoDBServer, oauth2Server, apiServer, communityViteServer].filter((s): s is NonNullable<typeof s> => s !== undefined); + return servers.map((server) => { + if ('getDiagnostics' in server && typeof server.getDiagnostics === 'function') { + return server.getDiagnostics(); + } + return { name: 'unknown-server', alive: false, pid: undefined, exitInfo: null, stdoutTail: '', stderrTail: '' }; + }); +} + +export async function probeApiHealth(): Promise<{ alive: boolean; status?: number; error?: string }> { + if (!apiUrl) return { alive: false, error: 'apiUrl not set' }; + try { + const res = await fetch(apiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: '{ __typename }' }), + signal: AbortSignal.timeout(3_000), + }); + return { alive: res.ok, status: res.status }; + } catch (err) { + return { alive: false, error: err instanceof Error ? err.message : String(err) }; + } +} + export async function stopAll(): Promise<void> { if (browseTheWeb) { await browseTheWeb.close().catch(() => undefined); From d90a284940966db600aacd771eed5a7c9a736d09 Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 11:02:35 -0400 Subject: [PATCH 18/38] diagnostic - go! --- .../e2e-tests/src/shared/support/hooks.ts | 69 +++++++++++-------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 52f9406e4..5d943ae7c 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -38,10 +38,15 @@ export function attachConsoleCapture(page: PlaywrightPage): void { const buffer: string[] = []; consoleMessagesByPage.set(page, buffer); page.on('console', (msg: ConsoleMessageLike) => { - buffer.push(`[${msg.type()}] ${msg.text()}`); + // Filter to high-signal levels — debug/info/log are mostly Vite/HMR/Apollo + // devtools noise that pushes interesting lines out of the tail. + const type = msg.type(); + if (type === 'error' || type === 'warning') { + buffer.push(`[${type}] ${msg.text().slice(0, 300)}`); + } }); page.on('pageerror', (err: Error) => { - buffer.push(`[pageerror] ${err.message}`); + buffer.push(`[pageerror] ${err.message.slice(0, 300)}`); }); page.on('requestfailed', (req: RequestLike) => { buffer.push(`[requestfailed] ${req.method()} ${req.url()} — ${req.failure()?.errorText ?? 'unknown'}`); @@ -51,8 +56,6 @@ export function attachConsoleCapture(page: PlaywrightPage): void { if (status < 400) return; const req = res.request(); const url = res.url(); - // Skip noisy static-asset 404s (favicons, source maps, vite client probes) - // to keep the diagnostic focused on API/data calls. const isStaticNoise = /\.(ico|png|svg|map|webp|woff2?)(\?|$)/i.test(url); if (isStaticNoise) { buffer.push(`[response ${status}] ${req.method()} ${url}`); @@ -60,18 +63,16 @@ export function attachConsoleCapture(page: PlaywrightPage): void { } let body = ''; try { - body = (await res.text()).slice(0, 500); + body = (await res.text()).slice(0, 200); } catch (err) { body = `<body unavailable: ${err instanceof Error ? err.message : 'unknown'}>`; } - const reqBody = req.postData()?.slice(0, 300) ?? ''; const reqHeaders = req.headers(); - const interestingHeaders = ['authorization', 'content-type', 'x-community-id', 'x-member-id', 'apollographql-client-name']; - const headerSummary = interestingHeaders - .map((h) => (reqHeaders[h] ? `${h}=${h === 'authorization' ? `${reqHeaders[h].slice(0, 20)}…(len=${reqHeaders[h].length})` : reqHeaders[h]}` : null)) - .filter(Boolean) - .join(', '); - buffer.push(`[response ${status}] ${req.method()} ${url}\n req-headers: ${headerSummary}\n req-body: ${reqBody}\n res-body: ${body}`); + const authHeader = reqHeaders['authorization']; + const auth = authHeader ? `auth(len=${authHeader.length})` : 'no-auth'; + const ct = reqHeaders['content-type'] ?? 'no-ct'; + const reqBodyHead = (req.postData() ?? '').slice(0, 60); + buffer.push(`[response ${status}] ${req.method()} ${url} | ${auth} ct=${ct} | reqStart=${reqBodyHead} | resBody=${body || '<empty>'}`); }); } @@ -112,14 +113,6 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) const { page } = browseTheWeb; const url = page.url(); const title = await page.title().catch(() => '<title unavailable>'); - const rootHtml = await page - .locator('#root') - .innerHTML({ timeout: 1_000 }) - .catch(() => '<#root unavailable>'); - const scriptSrcs = await page - .locator('script[src]') - .evaluateAll((els: Element[]) => els.map((el) => (el as HTMLScriptElement).src)) - .catch(() => [] as string[]); const bodyText = await page .locator('body') .innerText({ timeout: 1_000 }) @@ -138,28 +131,44 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) const serverSummary = serverDiagnostics .map((s) => { const exitDesc = s.exitInfo ? `EXITED code=${s.exitInfo.code} signal=${s.exitInfo.signal}` : s.alive ? `alive pid=${s.pid}` : 'no process'; - const stderrTail = s.stderrTail.trim() ? `\n stderr-tail:\n${indent(s.stderrTail.slice(-2000), ' ')}` : ''; - const stdoutTail = s.stdoutTail.trim() ? `\n stdout-tail:\n${indent(s.stdoutTail.slice(-1500), ' ')}` : ''; - return ` - ${s.name}: ${exitDesc}${stderrTail}${stdoutTail}`; + const stderrTail = s.stderrTail.trim() ? `\n stderr-tail:\n${indent(s.stderrTail.slice(-1200), ' ')}` : ''; + return ` - ${s.name}: ${exitDesc}${stderrTail}`; }) .join('\n'); + // Critical info (API health, subprocess state) is placed AT THE END + // of the diagnostic block so it survives Azure DevOps log display + // head-truncation. Less critical context is at the top. const diagnostic = [ `=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`, `URL: ${url}`, `Title: ${title}`, - `Script tags: ${JSON.stringify(scriptSrcs)}`, - `#root innerHTML (first 1000 chars): ${rootHtml.slice(0, 1000)}`, `Headings: ${JSON.stringify(headings)}`, `Input placeholders: ${JSON.stringify(placeholders)}`, - `Body text (first 500 chars): ${bodyText.slice(0, 500)}`, - `API health probe at failure: ${JSON.stringify(apiHealth)}`, - `Subprocess diagnostics:\n${serverSummary}`, - `Browser console (last 50 messages):`, - ...consoleLog.slice(-50), + `Body text (first 200 chars): ${bodyText.slice(0, 200)}`, + `Browser console (last 25 errors/warnings/responses):`, + ...consoleLog.slice(-25), + '', + `>>> CRITICAL: API HEALTH PROBE AT FAILURE: ${JSON.stringify(apiHealth)}`, + `>>> CRITICAL: SUBPROCESS DIAGNOSTICS:\n${serverSummary}`, '=== END DIAGNOSTICS ===', ].join('\n'); this.attach(diagnostic, 'text/plain'); process.stderr.write(`\n${diagnostic}\n`); + + // Belt-and-suspenders: emit critical lines individually so they + // survive log-display truncation that may swallow the larger + // cucumber attachment. + process.stderr.write(`\n[E2E-CRITICAL ${pickle.name}] api-health=${JSON.stringify(apiHealth)}\n`); + for (const s of serverDiagnostics) { + const exitDesc = s.exitInfo ? `EXITED code=${s.exitInfo.code} signal=${s.exitInfo.signal}` : s.alive ? `alive pid=${s.pid}` : 'no process'; + process.stderr.write(`[E2E-CRITICAL ${pickle.name}] subprocess ${s.name}: ${exitDesc}\n`); + if (s.stderrTail.trim()) { + const lines = s.stderrTail.slice(-1200).split('\n'); + for (const line of lines) { + process.stderr.write(`[E2E-CRITICAL ${s.name} stderr] ${line.slice(0, 400)}\n`); + } + } + } } } catch { /* Diagnostic capture is best-effort */ From 0cb97e9e7d770253bc917101dc43f49cde2bbd37 Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 11:35:44 -0400 Subject: [PATCH 19/38] diaganostics, once more --- .../e2e-tests/src/shared/support/hooks.ts | 32 ++++++++++--- .../shared/support/shared-infrastructure.ts | 48 +++++++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 5d943ae7c..f5a7f3ec0 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -6,7 +6,7 @@ import { After, AfterAll, Before, Status, setDefaultTimeout } from '@cucumber/cu import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { type CellixE2EWorld, stopSharedServers } from '../../world.ts'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; -import { getServerDiagnostics, probeApiHealth } from './shared-infrastructure.ts'; +import { getServerDiagnostics, probeApiHealth, probeApiVariants, probePortlessRoutes } from './shared-infrastructure.ts'; type PlaywrightPage = BrowseTheWeb['page']; @@ -81,9 +81,14 @@ const currentDir = fileURLToPath(new URL('.', import.meta.url)); /** Default scenario timeout from centralized configuration */ setDefaultTimeout(getTimeout('scenario')); -Before(async function (this: IWorld) { +Before(async function (this: IWorld, { pickle }: ITestCaseHookParameter) { const world = this as IWorld & CellixE2EWorld; await world.init(); + // Immediately after Before completes, probe the API. Tells us whether the + // 404s observed at After-time are because the API was *never* stable post + // startup, or because it transitioned to broken during scenario execution. + const postInitHealth = await probeApiHealth(); + process.stderr.write(`\n[E2E-CRITICAL ${pickle.name}] post-init-health=${JSON.stringify(postInitHealth)}\n`); }); After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) { @@ -127,12 +132,15 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) .catch(() => []); const consoleLog = consoleMessagesByPage.get(page) ?? []; const apiHealth = await probeApiHealth(); + const apiVariants = await probeApiVariants(); + const portlessRoutes = await probePortlessRoutes(); const serverDiagnostics = getServerDiagnostics(); const serverSummary = serverDiagnostics .map((s) => { const exitDesc = s.exitInfo ? `EXITED code=${s.exitInfo.code} signal=${s.exitInfo.signal}` : s.alive ? `alive pid=${s.pid}` : 'no process'; - const stderrTail = s.stderrTail.trim() ? `\n stderr-tail:\n${indent(s.stderrTail.slice(-1200), ' ')}` : ''; - return ` - ${s.name}: ${exitDesc}${stderrTail}`; + const stderrTail = s.stderrTail.trim() ? `\n stderr-tail:\n${indent(s.stderrTail.slice(-800), ' ')}` : ''; + const stdoutTail = s.stdoutTail.trim() ? `\n stdout-tail:\n${indent(s.stdoutTail.slice(-800), ' ')}` : ''; + return ` - ${s.name}: ${exitDesc}${stderrTail}${stdoutTail}`; }) .join('\n'); // Critical info (API health, subprocess state) is placed AT THE END @@ -149,6 +157,8 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) ...consoleLog.slice(-25), '', `>>> CRITICAL: API HEALTH PROBE AT FAILURE: ${JSON.stringify(apiHealth)}`, + `>>> CRITICAL: API VARIANT PROBES: ${JSON.stringify(apiVariants)}`, + `>>> CRITICAL: PORTLESS ROUTES:\n${indent(portlessRoutes, ' ')}`, `>>> CRITICAL: SUBPROCESS DIAGNOSTICS:\n${serverSummary}`, '=== END DIAGNOSTICS ===', ].join('\n'); @@ -159,13 +169,21 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) // survive log-display truncation that may swallow the larger // cucumber attachment. process.stderr.write(`\n[E2E-CRITICAL ${pickle.name}] api-health=${JSON.stringify(apiHealth)}\n`); + process.stderr.write(`[E2E-CRITICAL ${pickle.name}] api-variants=${JSON.stringify(apiVariants)}\n`); + for (const line of portlessRoutes.split('\n')) { + process.stderr.write(`[E2E-CRITICAL ${pickle.name}] portless-route ${line}\n`); + } for (const s of serverDiagnostics) { const exitDesc = s.exitInfo ? `EXITED code=${s.exitInfo.code} signal=${s.exitInfo.signal}` : s.alive ? `alive pid=${s.pid}` : 'no process'; process.stderr.write(`[E2E-CRITICAL ${pickle.name}] subprocess ${s.name}: ${exitDesc}\n`); if (s.stderrTail.trim()) { - const lines = s.stderrTail.slice(-1200).split('\n'); - for (const line of lines) { - process.stderr.write(`[E2E-CRITICAL ${s.name} stderr] ${line.slice(0, 400)}\n`); + for (const line of s.stderrTail.slice(-800).split('\n')) { + process.stderr.write(`[E2E-CRITICAL ${s.name} stderr] ${line.slice(0, 300)}\n`); + } + } + if (s.stdoutTail.trim()) { + for (const line of s.stdoutTail.slice(-800).split('\n')) { + process.stderr.write(`[E2E-CRITICAL ${s.name} stdout] ${line.slice(0, 300)}\n`); } } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index 87e8dc0ac..be0e6ea86 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -52,6 +52,54 @@ export async function probeApiHealth(): Promise<{ alive: boolean; status?: numbe } } +/** + * Probes a set of variant URLs (and the func admin endpoint) so we can see + * which return 200 / which 404 — distinguishes "route gone" from "API process + * gone" from "specific path unmatched". + */ +export async function probeApiVariants(): Promise<Record<string, { status?: number; error?: string }>> { + if (!apiUrl) return {}; + const base = apiUrl.replace(/\/api\/graphql.*$/, ''); + const variants: Array<{ label: string; url: string; method: 'GET' | 'POST'; body?: string }> = [ + { label: 'POST /api/graphql', url: `${base}/api/graphql`, method: 'POST', body: JSON.stringify({ query: '{ __typename }' }) }, + { label: 'POST /api/graphql/', url: `${base}/api/graphql/`, method: 'POST', body: JSON.stringify({ query: '{ __typename }' }) }, + { label: 'GET /api/graphql', url: `${base}/api/graphql`, method: 'GET' }, + { label: 'GET /admin/host/status', url: `${base}/admin/host/status`, method: 'GET' }, + { label: 'GET /', url: `${base}/`, method: 'GET' }, + ]; + const results: Record<string, { status?: number; error?: string }> = {}; + for (const v of variants) { + try { + const init: RequestInit = { + method: v.method, + signal: AbortSignal.timeout(3_000), + }; + if (v.body) { + init.headers = { 'Content-Type': 'application/json' }; + init.body = v.body; + } + const res = await fetch(v.url, init); + results[v.label] = { status: res.status }; + } catch (err) { + results[v.label] = { error: err instanceof Error ? err.message : String(err) }; + } + } + return results; +} + +export async function probePortlessRoutes(): Promise<string> { + const { execFile } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const exec = promisify(execFile); + const { getPortlessPath } = await import('./servers/resolve-portless.ts'); + try { + const { stdout } = await exec(getPortlessPath(), ['list'], { timeout: 5_000 }); + return stdout.slice(0, 1500); + } catch (err) { + return `<portless list failed: ${err instanceof Error ? err.message : String(err)}>`; + } +} + export async function stopAll(): Promise<void> { if (browseTheWeb) { await browseTheWeb.close().catch(() => undefined); From 8bad2b6f8c6cce99f99c295a514ceabe9f662e80 Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 12:05:33 -0400 Subject: [PATCH 20/38] once more - diagnose! --- .../e2e-tests/src/shared/support/hooks.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index f5a7f3ec0..52cbe7477 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -143,9 +143,11 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) return ` - ${s.name}: ${exitDesc}${stderrTail}${stdoutTail}`; }) .join('\n'); - // Critical info (API health, subprocess state) is placed AT THE END - // of the diagnostic block so it survives Azure DevOps log display - // head-truncation. Less critical context is at the top. + // One-line ULTIMATE SUMMARY at the very bottom — Azure DevOps log + // display has been hiding everything else. A single dense line is + // the only thing guaranteed to survive truncation. + const subprocessSummary = serverDiagnostics.map((s) => `${s.name}=${s.exitInfo ? `EXIT_${s.exitInfo.code}` : s.alive ? `alive_${s.pid}` : 'no_proc'}`).join(','); + const ultimateSummary = `ULTIMATE_SUMMARY api-health=${JSON.stringify(apiHealth)} variants=${JSON.stringify(apiVariants)} subprocesses=[${subprocessSummary}]`; const diagnostic = [ `=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`, `URL: ${url}`, @@ -161,6 +163,7 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) `>>> CRITICAL: PORTLESS ROUTES:\n${indent(portlessRoutes, ' ')}`, `>>> CRITICAL: SUBPROCESS DIAGNOSTICS:\n${serverSummary}`, '=== END DIAGNOSTICS ===', + ultimateSummary, ].join('\n'); this.attach(diagnostic, 'text/plain'); process.stderr.write(`\n${diagnostic}\n`); From c8afffbe84a2c1b0486efea0e3a93c894fd8a983 Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 12:29:09 -0400 Subject: [PATCH 21/38] another diagnosis --- .../e2e-tests/src/shared/support/hooks.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index 52cbe7477..fc80bfb98 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -143,11 +143,18 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) return ` - ${s.name}: ${exitDesc}${stderrTail}${stdoutTail}`; }) .join('\n'); - // One-line ULTIMATE SUMMARY at the very bottom — Azure DevOps log - // display has been hiding everything else. A single dense line is - // the only thing guaranteed to survive truncation. + // One-line ULTIMATE SUMMARY lines at the very bottom — Azure DevOps + // log display has been hiding everything else. Single dense lines + // are the only thing guaranteed to survive truncation. const subprocessSummary = serverDiagnostics.map((s) => `${s.name}=${s.exitInfo ? `EXIT_${s.exitInfo.code}` : s.alive ? `alive_${s.pid}` : 'no_proc'}`).join(','); - const ultimateSummary = `ULTIMATE_SUMMARY api-health=${JSON.stringify(apiHealth)} variants=${JSON.stringify(apiVariants)} subprocesses=[${subprocessSummary}]`; + const apiDiag = serverDiagnostics.find((s) => s.name === 'TestApiServer'); + const apiStdout = (apiDiag?.stdoutTail ?? '').slice(-1500).replaceAll('\n', '⏎'); + const apiStderr = (apiDiag?.stderrTail ?? '').slice(-800).replaceAll('\n', '⏎'); + const ultimateSummary = [ + `ULTIMATE_SUMMARY api-health=${JSON.stringify(apiHealth)} variants=${JSON.stringify(apiVariants)} subprocesses=[${subprocessSummary}]`, + `ULTIMATE_API_STDOUT_TAIL: ${apiStdout || '<empty>'}`, + `ULTIMATE_API_STDERR_TAIL: ${apiStderr || '<empty>'}`, + ].join('\n'); const diagnostic = [ `=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`, `URL: ${url}`, From 2ebf1fab467ed8f60cbe9c232d1d1eff537fdcb8 Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 13:31:19 -0400 Subject: [PATCH 22/38] once more, diagnose --- .../src/shared/support/servers/test-api-server.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 3755cb14e..825f19b48 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -41,6 +41,13 @@ export class TestApiServer extends PortlessServer { protected override get extraEnv() { return { + // Force dev mode so OtelBuilder uses console exporters and doesn't + // require APPLICATIONINSIGHTS_CONNECTION_STRING. CI agents may + // inherit NODE_ENV=production from pipeline variable groups, which + // causes the bundled entry point to throw at module load and func + // to register zero functions ("No job functions found"), surfacing + // as a 404 on /api/graphql even though the host is alive. + NODE_ENV: 'development', languageWorkers__node__arguments: '', COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), ACCOUNT_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, From 3b337eaa32f1142a5a0cb79c9934da67d11b610c Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Thu, 7 May 2026 14:18:56 -0400 Subject: [PATCH 23/38] adjustments for env variables --- .../e2e-tests/src/shared/support/servers/test-api-server.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 825f19b48..0d2f9a7e3 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -50,8 +50,14 @@ export class TestApiServer extends PortlessServer { NODE_ENV: 'development', languageWorkers__node__arguments: '', COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), + COSMOSDB_DBNAME: apiSettings.cosmosDbName, + // AZURE_STORAGE_CONNECTION_STRING is required by ServiceBlobStorage + // at appStart. Locally set via gitignored local.settings.json; absent + // in CI without this override. + AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', ACCOUNT_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, ACCOUNT_PORTAL_OIDC_ENDPOINT: apiSettings.accountPortalOidcEndpoint, + ACCOUNT_PORTAL_OIDC_AUDIENCE: apiSettings.accountPortalOidcAudience, ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', STAFF_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, STAFF_PORTAL_OIDC_ENDPOINT: apiSettings.accountPortalOidcEndpoint, From ce8e621daa4885747fc81747910eedcfae6c0d31 Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Fri, 8 May 2026 10:09:57 -0400 Subject: [PATCH 24/38] local fix for e2e broken from build pipeline changes --- .../src/index.ts | 10 -- .../ocom-verification/e2e-tests/cucumber.js | 2 +- .../e2e-tests/src/shared/support/hooks.ts | 168 +----------------- .../src/shared/support/servers/index.ts | 2 +- .../shared/support/servers/portless-server.ts | 46 ++--- .../shared/support/servers/test-api-server.ts | 14 +- .../servers/test-community-vite-server.ts | 4 +- .../support/servers/test-environment.ts | 11 ++ .../support/servers/test-oauth2-server.ts | 14 +- .../shared/support/shared-infrastructure.ts | 80 +-------- 10 files changed, 40 insertions(+), 311 deletions(-) diff --git a/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts b/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts index 1a4bf4794..13ed22359 100644 --- a/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts +++ b/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts @@ -12,12 +12,6 @@ export interface MongoMemoryReplicaSetDisposer { } export async function startMongoMemoryReplicaSet(config: MongoMemoryReplicaSetConfig): Promise<{ replicaSet: MongoMemoryReplSet; disposer: MongoMemoryReplicaSetDisposer }> { - console.log('Starting MongoDB Memory Replica Set', { - port: config.port, - dbName: config.dbName, - replSetName: config.replSetName, - }); - const replicaSet = await MongoMemoryReplSet.create({ binary: { version: config.binaryVersion ?? '7.0.14' }, replSet: { @@ -28,12 +22,8 @@ export async function startMongoMemoryReplicaSet(config: MongoMemoryReplicaSetCo instanceOpts: [{ port: config.port }], }); - const uri = replicaSet.getUri(config.dbName); - console.log('MongoDB Memory Replica Set ready at:', uri); - const disposer: MongoMemoryReplicaSetDisposer = { stop: async () => { - console.log('Stopping MongoDB Memory Replica Set'); await replicaSet.stop(); }, }; diff --git a/packages/ocom-verification/e2e-tests/cucumber.js b/packages/ocom-verification/e2e-tests/cucumber.js index 339a66b11..51912f713 100644 --- a/packages/ocom-verification/e2e-tests/cucumber.js +++ b/packages/ocom-verification/e2e-tests/cucumber.js @@ -7,5 +7,5 @@ export default { formatOptions: { snippetInterface: 'async-await', }, - parallel: 1, + parallel: 0, }; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts index fc80bfb98..477877fc7 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts @@ -6,98 +6,23 @@ import { After, AfterAll, Before, Status, setDefaultTimeout } from '@cucumber/cu import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { type CellixE2EWorld, stopSharedServers } from '../../world.ts'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; -import { getServerDiagnostics, probeApiHealth, probeApiVariants, probePortlessRoutes } from './shared-infrastructure.ts'; - -type PlaywrightPage = BrowseTheWeb['page']; - -const consoleMessagesByPage = new WeakMap<PlaywrightPage, string[]>(); - -function indent(text: string, prefix: string): string { - return text - .split('\n') - .map((line) => prefix + line) - .join('\n'); -} - -type ConsoleMessageLike = { type(): string; text(): string }; -type RequestLike = { - method(): string; - url(): string; - failure(): { errorText: string } | null; - headers(): Record<string, string>; - postData(): string | null; -}; -type ResponseLike = { - status(): number; - url(): string; - request(): RequestLike; - text(): Promise<string>; -}; - -export function attachConsoleCapture(page: PlaywrightPage): void { - const buffer: string[] = []; - consoleMessagesByPage.set(page, buffer); - page.on('console', (msg: ConsoleMessageLike) => { - // Filter to high-signal levels — debug/info/log are mostly Vite/HMR/Apollo - // devtools noise that pushes interesting lines out of the tail. - const type = msg.type(); - if (type === 'error' || type === 'warning') { - buffer.push(`[${type}] ${msg.text().slice(0, 300)}`); - } - }); - page.on('pageerror', (err: Error) => { - buffer.push(`[pageerror] ${err.message.slice(0, 300)}`); - }); - page.on('requestfailed', (req: RequestLike) => { - buffer.push(`[requestfailed] ${req.method()} ${req.url()} — ${req.failure()?.errorText ?? 'unknown'}`); - }); - page.on('response', async (res: ResponseLike) => { - const status = res.status(); - if (status < 400) return; - const req = res.request(); - const url = res.url(); - const isStaticNoise = /\.(ico|png|svg|map|webp|woff2?)(\?|$)/i.test(url); - if (isStaticNoise) { - buffer.push(`[response ${status}] ${req.method()} ${url}`); - return; - } - let body = ''; - try { - body = (await res.text()).slice(0, 200); - } catch (err) { - body = `<body unavailable: ${err instanceof Error ? err.message : 'unknown'}>`; - } - const reqHeaders = req.headers(); - const authHeader = reqHeaders['authorization']; - const auth = authHeader ? `auth(len=${authHeader.length})` : 'no-auth'; - const ct = reqHeaders['content-type'] ?? 'no-ct'; - const reqBodyHead = (req.postData() ?? '').slice(0, 60); - buffer.push(`[response ${status}] ${req.method()} ${url} | ${auth} ct=${ct} | reqStart=${reqBodyHead} | resBody=${body || '<empty>'}`); - }); -} const currentDir = fileURLToPath(new URL('.', import.meta.url)); /** Default scenario timeout from centralized configuration */ setDefaultTimeout(getTimeout('scenario')); -Before(async function (this: IWorld, { pickle }: ITestCaseHookParameter) { +Before(async function (this: IWorld) { const world = this as IWorld & CellixE2EWorld; await world.init(); - // Immediately after Before completes, probe the API. Tells us whether the - // 404s observed at After-time are because the API was *never* stable post - // startup, or because it transitioned to broken during scenario execution. - const postInitHealth = await probeApiHealth(); - process.stderr.write(`\n[E2E-CRITICAL ${pickle.name}] post-init-health=${JSON.stringify(postInitHealth)}\n`); }); After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) { const world = this as IWorld & CellixE2EWorld; if (result?.status === Status.FAILED) { - const browseTheWeb = BrowseTheWeb.current(); - try { + const browseTheWeb = BrowseTheWeb.current(); if (browseTheWeb) { const reportsDir = path.resolve(currentDir, '..', '..', '..', 'reports', 'screenshots'); fs.mkdirSync(reportsDir, { recursive: true }); @@ -112,95 +37,6 @@ After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) } catch { /* Screenshot capture is best-effort */ } - - try { - if (browseTheWeb) { - const { page } = browseTheWeb; - const url = page.url(); - const title = await page.title().catch(() => '<title unavailable>'); - const bodyText = await page - .locator('body') - .innerText({ timeout: 1_000 }) - .catch(() => '<body text unavailable>'); - const placeholders = await page - .locator('input, textarea') - .evaluateAll((els: Element[]) => els.map((el) => (el as HTMLInputElement | HTMLTextAreaElement).placeholder || '<no placeholder>')) - .catch(() => [] as string[]); - const headings = await page - .locator('h1, h2, h3') - .allTextContents() - .catch(() => []); - const consoleLog = consoleMessagesByPage.get(page) ?? []; - const apiHealth = await probeApiHealth(); - const apiVariants = await probeApiVariants(); - const portlessRoutes = await probePortlessRoutes(); - const serverDiagnostics = getServerDiagnostics(); - const serverSummary = serverDiagnostics - .map((s) => { - const exitDesc = s.exitInfo ? `EXITED code=${s.exitInfo.code} signal=${s.exitInfo.signal}` : s.alive ? `alive pid=${s.pid}` : 'no process'; - const stderrTail = s.stderrTail.trim() ? `\n stderr-tail:\n${indent(s.stderrTail.slice(-800), ' ')}` : ''; - const stdoutTail = s.stdoutTail.trim() ? `\n stdout-tail:\n${indent(s.stdoutTail.slice(-800), ' ')}` : ''; - return ` - ${s.name}: ${exitDesc}${stderrTail}${stdoutTail}`; - }) - .join('\n'); - // One-line ULTIMATE SUMMARY lines at the very bottom — Azure DevOps - // log display has been hiding everything else. Single dense lines - // are the only thing guaranteed to survive truncation. - const subprocessSummary = serverDiagnostics.map((s) => `${s.name}=${s.exitInfo ? `EXIT_${s.exitInfo.code}` : s.alive ? `alive_${s.pid}` : 'no_proc'}`).join(','); - const apiDiag = serverDiagnostics.find((s) => s.name === 'TestApiServer'); - const apiStdout = (apiDiag?.stdoutTail ?? '').slice(-1500).replaceAll('\n', '⏎'); - const apiStderr = (apiDiag?.stderrTail ?? '').slice(-800).replaceAll('\n', '⏎'); - const ultimateSummary = [ - `ULTIMATE_SUMMARY api-health=${JSON.stringify(apiHealth)} variants=${JSON.stringify(apiVariants)} subprocesses=[${subprocessSummary}]`, - `ULTIMATE_API_STDOUT_TAIL: ${apiStdout || '<empty>'}`, - `ULTIMATE_API_STDERR_TAIL: ${apiStderr || '<empty>'}`, - ].join('\n'); - const diagnostic = [ - `=== E2E FAILURE DIAGNOSTICS: ${pickle.name} ===`, - `URL: ${url}`, - `Title: ${title}`, - `Headings: ${JSON.stringify(headings)}`, - `Input placeholders: ${JSON.stringify(placeholders)}`, - `Body text (first 200 chars): ${bodyText.slice(0, 200)}`, - `Browser console (last 25 errors/warnings/responses):`, - ...consoleLog.slice(-25), - '', - `>>> CRITICAL: API HEALTH PROBE AT FAILURE: ${JSON.stringify(apiHealth)}`, - `>>> CRITICAL: API VARIANT PROBES: ${JSON.stringify(apiVariants)}`, - `>>> CRITICAL: PORTLESS ROUTES:\n${indent(portlessRoutes, ' ')}`, - `>>> CRITICAL: SUBPROCESS DIAGNOSTICS:\n${serverSummary}`, - '=== END DIAGNOSTICS ===', - ultimateSummary, - ].join('\n'); - this.attach(diagnostic, 'text/plain'); - process.stderr.write(`\n${diagnostic}\n`); - - // Belt-and-suspenders: emit critical lines individually so they - // survive log-display truncation that may swallow the larger - // cucumber attachment. - process.stderr.write(`\n[E2E-CRITICAL ${pickle.name}] api-health=${JSON.stringify(apiHealth)}\n`); - process.stderr.write(`[E2E-CRITICAL ${pickle.name}] api-variants=${JSON.stringify(apiVariants)}\n`); - for (const line of portlessRoutes.split('\n')) { - process.stderr.write(`[E2E-CRITICAL ${pickle.name}] portless-route ${line}\n`); - } - for (const s of serverDiagnostics) { - const exitDesc = s.exitInfo ? `EXITED code=${s.exitInfo.code} signal=${s.exitInfo.signal}` : s.alive ? `alive pid=${s.pid}` : 'no process'; - process.stderr.write(`[E2E-CRITICAL ${pickle.name}] subprocess ${s.name}: ${exitDesc}\n`); - if (s.stderrTail.trim()) { - for (const line of s.stderrTail.slice(-800).split('\n')) { - process.stderr.write(`[E2E-CRITICAL ${s.name} stderr] ${line.slice(0, 300)}\n`); - } - } - if (s.stdoutTail.trim()) { - for (const line of s.stdoutTail.slice(-800).split('\n')) { - process.stderr.write(`[E2E-CRITICAL ${s.name} stdout] ${line.slice(0, 300)}\n`); - } - } - } - } - } catch { - /* Diagnostic capture is best-effort */ - } } await world.cleanup(); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts index 8301c3a3f..a5f0e385e 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts @@ -2,5 +2,5 @@ export { MongoDBTestServer } from '@ocom-verification/verification-shared/server export { PortlessServer } from './portless-server.ts'; export { TestApiServer } from './test-api-server.ts'; export { TestCommunityViteServer } from './test-community-vite-server.ts'; -export { buildUrl, cleanupTestEnvironment, initTestEnvironment, setMongoConnectionString } from './test-environment.ts'; +export { buildUrl, cleanupTestEnvironment, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, setMongoConnectionString } from './test-environment.ts'; export { TestOAuth2Server } from './test-oauth2-server.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index 0092a91bd..33d834460 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -17,20 +17,10 @@ import { getTimeout } from '@ocom-verification/verification-shared/settings'; * * For faster API tests, use GraphQLTestServer instead. */ -const LOG_TAIL_BYTES = 16_384; - -function appendCapped(existing: string, chunk: string): string { - const combined = existing + chunk; - return combined.length > LOG_TAIL_BYTES ? combined.slice(-LOG_TAIL_BYTES) : combined; -} - export abstract class PortlessServer implements TestServer { private process: ChildProcess | null = null; private startedByUs = false; private readonly useDetachedProcessGroup = process.platform !== 'win32'; - private capturedStdout = ''; - private capturedStderr = ''; - private exitInfo: { code: number | null; signal: NodeJS.Signals | null } | null = null; protected abstract get probeUrl(): string; protected abstract get readyMarker(): string; @@ -108,7 +98,12 @@ export abstract class PortlessServer implements TestServer { this.process = null; this.startedByUs = false; - this.killProcess(proc, 'SIGTERM'); + // SIGINT (the same signal Ctrl+C sends in `pnpm dev`) lets portless's + // CLI run its cleanup branch — deregister the hostname from + // ~/.portless/routes.json before exiting. SIGTERM skips that handler in + // some tools and leaves stale state. Fall back to SIGKILL after the + // shutdown timeout for anything that ignores SIGINT. + this.killProcess(proc, 'SIGINT'); const shutdownTimeout = getTimeout('serverShutdown'); await new Promise<void>((resolve) => { @@ -146,21 +141,6 @@ export abstract class PortlessServer implements TestServer { return getTimeout('serverStartup'); } - /** - * Diagnostic snapshot for failure reporting. Includes whether the underlying - * process is still alive and the most recent stdout/stderr captured. - */ - getDiagnostics(): { name: string; alive: boolean; pid: number | undefined; exitInfo: { code: number | null; signal: NodeJS.Signals | null } | null; stdoutTail: string; stderrTail: string } { - return { - name: this.serverName, - alive: this.process !== null && this.exitInfo === null, - pid: this.process?.pid, - exitInfo: this.exitInfo, - stdoutTail: this.capturedStdout, - stderrTail: this.capturedStderr, - }; - } - private waitForReady(): Promise<void> { return new Promise((resolve, reject) => { const proc = this.process; @@ -174,6 +154,7 @@ export abstract class PortlessServer implements TestServer { reject(new Error(`${this.serverName} did not start within ${startupTimeout}ms`)); }, startupTimeout); + let stderrOutput = ''; let ready = false; const resolveWhenReachable = () => { @@ -193,18 +174,17 @@ export abstract class PortlessServer implements TestServer { }); }; - // stdout/stderr listeners persist beyond startup so we keep collecting - // logs for post-failure diagnostics. Buffers are size-capped. + // stdout/stderr listeners detect the readyMarker and collect stderr + // for error reporting if the process exits unexpectedly. proc.stdout?.on('data', (data: Buffer) => { const text = data.toString(); - this.capturedStdout = appendCapped(this.capturedStdout, text); if (text.includes(this.readyMarker)) { resolveWhenReachable(); } }); proc.stderr?.on('data', (data: Buffer) => { - this.capturedStderr = appendCapped(this.capturedStderr, data.toString()); + stderrOutput += data.toString(); }); proc.on('error', (err) => { @@ -216,13 +196,9 @@ export abstract class PortlessServer implements TestServer { proc.on('exit', (code, signal) => { clearTimeout(timeout); - this.exitInfo = { code, signal }; this.process = null; this.startedByUs = false; - // Reject is a no-op once the promise has settled, but the exitInfo - // + log buffers above let the After hook surface a post-startup - // crash on the next failed scenario. - reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}). stderr: ${this.capturedStderr.slice(-2000)}`)); + reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); }); }); } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 0d2f9a7e3..cfb88f43c 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -1,6 +1,6 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl, getMongoConnectionString } from './test-environment.ts'; +import { buildUrl, getMongoConnectionString, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; export class TestApiServer extends PortlessServer { protected get probeUrl() { @@ -55,13 +55,13 @@ export class TestApiServer extends PortlessServer { // at appStart. Locally set via gitignored local.settings.json; absent // in CI without this override. AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', - ACCOUNT_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, - ACCOUNT_PORTAL_OIDC_ENDPOINT: apiSettings.accountPortalOidcEndpoint, - ACCOUNT_PORTAL_OIDC_AUDIENCE: apiSettings.accountPortalOidcAudience, + ACCOUNT_PORTAL_OIDC_ISSUER: mockOidcIssuer, + ACCOUNT_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, + ACCOUNT_PORTAL_OIDC_AUDIENCE: mockOidcAudience, ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', - STAFF_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, - STAFF_PORTAL_OIDC_ENDPOINT: apiSettings.accountPortalOidcEndpoint, - STAFF_PORTAL_OIDC_AUDIENCE: apiSettings.accountPortalOidcAudience, + STAFF_PORTAL_OIDC_ISSUER: mockOidcIssuer, + STAFF_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, + STAFF_PORTAL_OIDC_AUDIENCE: mockOidcAudience, STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', VITE_FUNCTION_ENDPOINT: buildUrl('data-access.ownercommunity.localhost', '/api/graphql'), }; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index 10f6cd0f7..e1f759151 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -1,6 +1,6 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl } from './test-environment.ts'; +import { buildUrl, mockOidcIssuer } from './test-environment.ts'; /** * Starts the community (user) portal Vite dev server as a subprocess via `pnpm run dev`. @@ -40,7 +40,7 @@ export class TestCommunityViteServer extends PortlessServer { BROWSER: 'none', NODE_ENV: 'development', VITE_BASE_URL: uiBase, - VITE_AAD_B2C_ACCOUNT_AUTHORITY: apiSettings.accountPortalOidcIssuer, + VITE_AAD_B2C_ACCOUNT_AUTHORITY: mockOidcIssuer, VITE_AAD_B2C_REDIRECT_URI: `${uiBase}/auth-redirect`, VITE_FUNCTION_ENDPOINT: apiEndpoint, }; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 3c31f78d9..906d75289 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -22,6 +22,17 @@ export function buildUrl(hostname: string, path = ''): string { return `https://${hostname}:1355${path}`; } +/** + * Mock OIDC URLs derived from the portless hostname and the portal name + * registered by server-oauth2-mock (via apps/ui-community/mock-oidc.json). + * + * These are hardcoded here so the e2e test infrastructure is self-contained + * and does not depend on potentially-stale local.settings.json values. + */ +export const mockOidcIssuer = buildUrl('mock-auth.ownercommunity.localhost', '/community'); +export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; +export const mockOidcAudience = 'mock-client'; + export function setMongoConnectionString(connStr: string): void { mongoConnectionString = connStr; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index 10c24993c..b41541bf0 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -1,10 +1,10 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl } from './test-environment.ts'; +import { buildUrl, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; export class TestOAuth2Server extends PortlessServer { protected get probeUrl() { - return apiSettings.accountPortalOidcEndpoint; + return mockOidcEndpoint; } protected get readyMarker() { return 'Registered OIDC config'; @@ -13,14 +13,6 @@ export class TestOAuth2Server extends PortlessServer { return 'TestOAuth2Server'; } - /** - * OAuth2 mock server is lightweight and starts very quickly. - * Using a short timeout (30s vs default 120s) for faster feedback. - */ - protected override get startupTimeoutMs() { - return 30_000; - } - protected get spawnArgs() { return ['run', 'dev']; } @@ -29,7 +21,7 @@ export class TestOAuth2Server extends PortlessServer { } getUrl(): string { - return apiSettings.accountPortalOidcIssuer; + return mockOidcIssuer; } async generateAccessToken(_audience = 'mock-client'): Promise<string> { diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index be0e6ea86..4fb0900c4 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,9 +1,7 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; -import { attachConsoleCapture } from './hooks.ts'; import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, mockOidcAudience, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; let mongoDBServer: MongoDBTestServer | undefined; let oauth2Server: TestOAuth2Server | undefined; @@ -27,79 +25,6 @@ export function getState(): InfrastructureState { return { apiUrl, accessToken, browseTheWeb }; } -export function getServerDiagnostics() { - const servers = [mongoDBServer, oauth2Server, apiServer, communityViteServer].filter((s): s is NonNullable<typeof s> => s !== undefined); - return servers.map((server) => { - if ('getDiagnostics' in server && typeof server.getDiagnostics === 'function') { - return server.getDiagnostics(); - } - return { name: 'unknown-server', alive: false, pid: undefined, exitInfo: null, stdoutTail: '', stderrTail: '' }; - }); -} - -export async function probeApiHealth(): Promise<{ alive: boolean; status?: number; error?: string }> { - if (!apiUrl) return { alive: false, error: 'apiUrl not set' }; - try { - const res = await fetch(apiUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: '{ __typename }' }), - signal: AbortSignal.timeout(3_000), - }); - return { alive: res.ok, status: res.status }; - } catch (err) { - return { alive: false, error: err instanceof Error ? err.message : String(err) }; - } -} - -/** - * Probes a set of variant URLs (and the func admin endpoint) so we can see - * which return 200 / which 404 — distinguishes "route gone" from "API process - * gone" from "specific path unmatched". - */ -export async function probeApiVariants(): Promise<Record<string, { status?: number; error?: string }>> { - if (!apiUrl) return {}; - const base = apiUrl.replace(/\/api\/graphql.*$/, ''); - const variants: Array<{ label: string; url: string; method: 'GET' | 'POST'; body?: string }> = [ - { label: 'POST /api/graphql', url: `${base}/api/graphql`, method: 'POST', body: JSON.stringify({ query: '{ __typename }' }) }, - { label: 'POST /api/graphql/', url: `${base}/api/graphql/`, method: 'POST', body: JSON.stringify({ query: '{ __typename }' }) }, - { label: 'GET /api/graphql', url: `${base}/api/graphql`, method: 'GET' }, - { label: 'GET /admin/host/status', url: `${base}/admin/host/status`, method: 'GET' }, - { label: 'GET /', url: `${base}/`, method: 'GET' }, - ]; - const results: Record<string, { status?: number; error?: string }> = {}; - for (const v of variants) { - try { - const init: RequestInit = { - method: v.method, - signal: AbortSignal.timeout(3_000), - }; - if (v.body) { - init.headers = { 'Content-Type': 'application/json' }; - init.body = v.body; - } - const res = await fetch(v.url, init); - results[v.label] = { status: res.status }; - } catch (err) { - results[v.label] = { error: err instanceof Error ? err.message : String(err) }; - } - } - return results; -} - -export async function probePortlessRoutes(): Promise<string> { - const { execFile } = await import('node:child_process'); - const { promisify } = await import('node:util'); - const exec = promisify(execFile); - const { getPortlessPath } = await import('./servers/resolve-portless.ts'); - try { - const { stdout } = await exec(getPortlessPath(), ['list'], { timeout: 5_000 }); - return stdout.slice(0, 1500); - } catch (err) { - return `<portless list failed: ${err instanceof Error ? err.message : String(err)}>`; - } -} - export async function stopAll(): Promise<void> { if (browseTheWeb) { await browseTheWeb.close().catch(() => undefined); @@ -171,7 +96,7 @@ export async function ensureE2EServers(): Promise<void> { } if (!accessToken) { phase2.push( - oauth2.generateAccessToken(apiSettings.accountPortalOidcAudience).then((token) => { + oauth2.generateAccessToken(mockOidcAudience).then((token) => { accessToken = token; }), ); @@ -208,7 +133,6 @@ async function ensureAuthenticatedBrowserContext(options: { baseURL?: string; ig } const seedPage = await authenticatedBrowserContext.newPage(); - attachConsoleCapture(seedPage); try { if (options.performLogin) { From c7d81c76f30a4464c54a480c2057224ec55adfda Mon Sep 17 00:00:00 2001 From: Jason Morais <jasonmorais02@gmail.com> Date: Fri, 8 May 2026 15:41:30 -0400 Subject: [PATCH 25/38] forcing commit to validate coverage of backend files due to lack fo staging for sonar coverage to pick up --- build-pipeline/scripts/merge-coverage.js | 9 ++++--- packages/cellix/archunit-tests/package.json | 1 - .../acceptance-api/.c8rc.json | 13 +--------- .../acceptance-ui/.c8rc.json | 18 ++++++++++++- .../ocom/application-services/package.json | 1 - .../ocom/application-services/src/index.ts | 6 ++++- packages/ocom/archunit-tests/package.json | 1 - packages/ocom/domain/package.json | 1 - .../contexts/community/community/community.ts | 7 ++--- packages/ocom/graphql/package.json | 1 - .../src/schema/types/member.resolvers.ts | 5 ++-- packages/ocom/persistence/package.json | 1 - .../ui-community-route-accounts/package.json | 1 - .../components/community-create.container.tsx | 5 ++-- .../src/components/community-create.tsx | 7 ++++- .../ui-community-route-admin/package.json | 1 - .../src/components/settings-general.tsx | 3 ++- .../ocom/ui-community-route-root/package.json | 1 - .../ocom/ui-community-shared/package.json | 1 - packages/ocom/ui-shared/package.json | 1 - .../molecules/logged-in-user/logged-in.tsx | 2 ++ .../package.json | 1 - .../ocom/ui-staff-route-finance/package.json | 1 - .../ocom/ui-staff-route-finance/src/index.tsx | 6 +++-- .../ocom/ui-staff-route-root/package.json | 1 - .../ui-staff-route-tech-admin/package.json | 1 - .../package.json | 1 - packages/ocom/ui-staff-shared/package.json | 1 - sonar-project.properties | 26 ++++++++++++++++--- 29 files changed, 74 insertions(+), 50 deletions(-) diff --git a/build-pipeline/scripts/merge-coverage.js b/build-pipeline/scripts/merge-coverage.js index 2aa2454a1..50ba79dbe 100755 --- a/build-pipeline/scripts/merge-coverage.js +++ b/build-pipeline/scripts/merge-coverage.js @@ -49,17 +49,18 @@ function mergeLcovFiles() { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { - if (entry.name !== 'node_modules' && entry.name !== '.git') { + if (!['node_modules', '.git', '.turbo', 'dist', 'build'].includes(entry.name)) { findLcovFiles(fullPath); } } else if (entry.name === 'lcov.info' && fullPath.replaceAll('\\', '/').includes('/coverage/')) { - lcovFiles.push(fullPath); + if (fullPath !== outputFile) { + lcovFiles.push(fullPath); + } } } } - // Search only in the acceptance/verification packages for coverage - const searchDirs = ['packages/ocom-verification'].filter((dir) => fs.existsSync(path.join(rootDir, dir))); + const searchDirs = ['apps', 'packages'].filter((dir) => fs.existsSync(path.join(rootDir, dir))); for (const dir of searchDirs) { findLcovFiles(path.join(rootDir, dir)); diff --git a/packages/cellix/archunit-tests/package.json b/packages/cellix/archunit-tests/package.json index 46b33943c..9a37d4b97 100644 --- a/packages/cellix/archunit-tests/package.json +++ b/packages/cellix/archunit-tests/package.json @@ -6,7 +6,6 @@ "type": "module", "scripts": { "test": "vitest run --silent --reporter=dot", - "test:coverage": "pnpm run test", "test:watch": "vitest" }, "devDependencies": { diff --git a/packages/ocom-verification/acceptance-api/.c8rc.json b/packages/ocom-verification/acceptance-api/.c8rc.json index 64101073f..a740072f6 100644 --- a/packages/ocom-verification/acceptance-api/.c8rc.json +++ b/packages/ocom-verification/acceptance-api/.c8rc.json @@ -1,17 +1,6 @@ { "allowExternal": true, - "include": [ - "**/ocom/application-services/dist/**", - "**/ocom/context-spec/dist/**", - "**/ocom/domain/dist/**", - "**/ocom/graphql/dist/**", - "**/ocom/graphql-handler/dist/**", - "**/ocom/persistence/dist/**", - "**/ocom/service-apollo-server/dist/**", - "**/ocom/service-mongoose/dist/**", - "**/ocom/service-token-validation/dist/**", - "**/ocom/data-sources-mongoose-models/dist/**" - ], + "include": ["**/ocom/application-services/dist/**", "**/ocom/domain/dist/**", "**/ocom/graphql/dist/**", "**/ocom/persistence/dist/**", "**/ocom/data-sources-mongoose-models/dist/**"], "exclude": ["**/node_modules/**"], "reporter": ["text", "lcovonly"], "report-dir": "coverage" diff --git a/packages/ocom-verification/acceptance-ui/.c8rc.json b/packages/ocom-verification/acceptance-ui/.c8rc.json index 795f70cde..74f57fda1 100644 --- a/packages/ocom-verification/acceptance-ui/.c8rc.json +++ b/packages/ocom-verification/acceptance-ui/.c8rc.json @@ -1,9 +1,25 @@ { + "all": true, "allowExternal": true, + "src": [ + "../../ocom/ui-community-route-accounts/src", + "../../ocom/ui-community-route-admin/src", + "../../ocom/ui-community-route-root/src", + "../../ocom/ui-community-route-shared/src", + "../../ocom/ui-community-shared/src", + "../../ocom/ui-shared/src", + "../../ocom/ui-staff-route-community-management/src", + "../../ocom/ui-staff-route-finance/src", + "../../ocom/ui-staff-route-root/src", + "../../ocom/ui-staff-route-tech-admin/src", + "../../ocom/ui-staff-route-user-management/src", + "../../ocom/ui-staff-shared/src" + ], "include": [ "**/ocom/ui-community-route-accounts/src/**", "**/ocom/ui-community-route-admin/src/**", "**/ocom/ui-community-route-root/src/**", + "**/ocom/ui-community-route-shared/src/**", "**/ocom/ui-community-shared/src/**", "**/ocom/ui-shared/src/**", "**/ocom/ui-staff-route-community-management/src/**", @@ -13,7 +29,7 @@ "**/ocom/ui-staff-route-user-management/src/**", "**/ocom/ui-staff-shared/src/**" ], - "exclude": ["**/node_modules/**", "**/*.generated.*", "**/*.d.ts", "**/*.stories.*", "**/*.test.*", "**/*.spec.*"], + "exclude": ["**/node_modules/**", "**/*.generated.*", "**/generated.*", "**/*.d.ts", "**/*.stories.*", "**/*.test.*", "**/*.spec.*"], "reporter": ["text", "lcovonly"], "report-dir": "coverage" } diff --git a/packages/ocom/application-services/package.json b/packages/ocom/application-services/package.json index 7141c448b..8d6a43d88 100644 --- a/packages/ocom/application-services/package.json +++ b/packages/ocom/application-services/package.json @@ -19,7 +19,6 @@ "build": "tsgo --build", "watch": "tsgo --watch", "test": "vitest run", - "test:coverage": "vitest run --coverage", "test:watch": "vitest", "lint": "biome lint", "clean": "rimraf dist" diff --git a/packages/ocom/application-services/src/index.ts b/packages/ocom/application-services/src/index.ts index ccfdc57d1..b8d413ebe 100644 --- a/packages/ocom/application-services/src/index.ts +++ b/packages/ocom/application-services/src/index.ts @@ -44,7 +44,8 @@ export type ApplicationServicesFactory = AppServicesHost<ApplicationServices>; export const buildApplicationServicesFactory = (infrastructureServicesRegistry: ApiContextSpec): ApplicationServicesFactory => { const forRequest = async (rawAuthHeader?: string, hints?: PrincipalHints): Promise<ApplicationServices> => { - const accessToken = rawAuthHeader?.replace(/^Bearer\s+/i, '').trim(); + const normalizedAuthHeader = rawAuthHeader?.trim(); + const accessToken = normalizedAuthHeader?.replace(/^Bearer\s+/i, '').trim(); const tokenValidationResult = accessToken ? await infrastructureServicesRegistry.tokenValidationService.verifyJwt<VerifiedJwt>(accessToken) : null; let passport = Domain.PassportFactory.forGuest(); if (tokenValidationResult !== null) { @@ -59,6 +60,9 @@ export const buildApplicationServicesFactory = (infrastructureServicesRegistry: passport = Domain.PassportFactory.forMember(endUser, member, community); } } else if (openIdConfigKey === 'StaffPortal') { + if (verifiedJwt.sub.trim().length === 0) { + passport = Domain.PassportFactory.forGuest(); + } const staffUser = undefined; // const staffUser = await readonlyDataSource.User.StaffUser.StaffUserReadRepo.getByExternalId(verifiedJwt.sub); if (staffUser) { diff --git a/packages/ocom/archunit-tests/package.json b/packages/ocom/archunit-tests/package.json index ee545570d..0687f4431 100644 --- a/packages/ocom/archunit-tests/package.json +++ b/packages/ocom/archunit-tests/package.json @@ -6,7 +6,6 @@ "type": "module", "scripts": { "test": "vitest run --silent --reporter=dot", - "test:coverage": "pnpm run test", "test:watch": "vitest" }, "devDependencies": { diff --git a/packages/ocom/domain/package.json b/packages/ocom/domain/package.json index 6258e16d6..e070870e7 100644 --- a/packages/ocom/domain/package.json +++ b/packages/ocom/domain/package.json @@ -19,7 +19,6 @@ "build": "tsgo --build", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:acceptance": "cucumber-js", "test:integration": "vitest run integration.test.ts --silent --reporter=dot", "test:serenity": "cucumber-js", diff --git a/packages/ocom/domain/src/domain/contexts/community/community/community.ts b/packages/ocom/domain/src/domain/contexts/community/community/community.ts index 49b9eb5d1..2b96106c9 100644 --- a/packages/ocom/domain/src/domain/contexts/community/community/community.ts +++ b/packages/ocom/domain/src/domain/contexts/community/community/community.ts @@ -1,6 +1,6 @@ import { AggregateRoot } from '@cellix/domain-seedwork/aggregate-root'; -import { PermissionError } from '@cellix/domain-seedwork/domain-entity'; import type { DomainEntityProps } from '@cellix/domain-seedwork/domain-entity'; +import { PermissionError } from '@cellix/domain-seedwork/domain-entity'; import { CommunityCreatedEvent, type CommunityCreatedProps } from '../../../events/types/community-created.ts'; import { CommunityDomainUpdatedEvent, type CommunityDomainUpdatedProps } from '../../../events/types/community-domain-updated.ts'; import { CommunityWhiteLabelDomainUpdatedEvent, type CommunityWhiteLabelDomainUpdatedProps } from '../../../events/types/community-white-label-domain-updated.ts'; @@ -80,10 +80,11 @@ export class Community<props extends CommunityProps> extends AggregateRoot<props } const oldDomain = this.props.domain; if (this.props.domain !== domain) { - this.props.domain = new ValueObjects.Domain(domain).valueOf(); + const nextDomain = domain; + this.props.domain = new ValueObjects.Domain(nextDomain).valueOf(); this.addIntegrationEvent<CommunityDomainUpdatedProps, CommunityDomainUpdatedEvent>(CommunityDomainUpdatedEvent, { communityId: this.props.id, - domain, + domain: nextDomain, oldDomain: oldDomain, }); } diff --git a/packages/ocom/graphql/package.json b/packages/ocom/graphql/package.json index 57536dcd5..2a6f275e8 100644 --- a/packages/ocom/graphql/package.json +++ b/packages/ocom/graphql/package.json @@ -21,7 +21,6 @@ "build": "tsgo --build", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest", "clean": "rimraf dist **/*.generated.ts **/graphql.schema.json" }, diff --git a/packages/ocom/graphql/src/schema/types/member.resolvers.ts b/packages/ocom/graphql/src/schema/types/member.resolvers.ts index 2fcb81bd5..5808db073 100644 --- a/packages/ocom/graphql/src/schema/types/member.resolvers.ts +++ b/packages/ocom/graphql/src/schema/types/member.resolvers.ts @@ -35,7 +35,8 @@ import type { GraphContext } from '../context.ts'; * Used to enforce self-protection guards on destructive mutations. */ const getActorMemberIdForCommunity = async (context: GraphContext, communityId?: string): Promise<string | null> => { - if (!communityId) { + const requestedCommunityId = communityId; + if (!requestedCommunityId) { return null; } @@ -51,7 +52,7 @@ const getActorMemberIdForCommunity = async (context: GraphContext, communityId?: try { const members = await queryByEndUserExternalId({ externalId }); - const found = members.find((m) => String(m.communityId) === String(communityId)); + const found = members.find((m) => String(m.communityId) === String(requestedCommunityId)); return found ? String(found.id) : null; } catch { return null; diff --git a/packages/ocom/persistence/package.json b/packages/ocom/persistence/package.json index 6dcfde42e..5613144f2 100644 --- a/packages/ocom/persistence/package.json +++ b/packages/ocom/persistence/package.json @@ -115,7 +115,6 @@ "build": "tsgo --build", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest", "lint": "biome lint", "clean": "rimraf dist" diff --git a/packages/ocom/ui-community-route-accounts/package.json b/packages/ocom/ui-community-route-accounts/package.json index c81ad6b8e..ebb210df5 100644 --- a/packages/ocom/ui-community-route-accounts/package.json +++ b/packages/ocom/ui-community-route-accounts/package.json @@ -11,7 +11,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-community-route-accounts/src/components/community-create.container.tsx b/packages/ocom/ui-community-route-accounts/src/components/community-create.container.tsx index bce42ff44..73ed72b01 100644 --- a/packages/ocom/ui-community-route-accounts/src/components/community-create.container.tsx +++ b/packages/ocom/ui-community-route-accounts/src/components/community-create.container.tsx @@ -47,9 +47,10 @@ export const CommunityCreateContainer: React.FC = () => { const content = () => { if (loading) { - return <div>Loading...</div>; + return <div aria-live="polite">Loading...</div>; } else if (error) { - return <div>Error {JSON.stringify(error)}</div>; + const errorMessage = JSON.stringify(error); + return <div role="alert">Error {errorMessage}</div>; } else { return <CommunityCreate onSave={handleSave} />; } diff --git a/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx b/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx index 9d1d703ee..69013dd1d 100644 --- a/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx +++ b/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx @@ -14,6 +14,7 @@ export const CommunityCreate: React.FC<CommunityCreateProps> = (props) => { token: { colorTextBase, colorBgContainer }, } = theme.useToken(); const { Title } = Typography; + const introText = 'Getting started with your community is only a few clicks away.'; return ( <> <div @@ -28,7 +29,7 @@ export const CommunityCreate: React.FC<CommunityCreateProps> = (props) => { </Helmet> <Title level={3}>Creating your Community

- Getting started with your community is only a few clicks away. + {introText}
Once you create it here you'll see it in the list of communities you have access to.
You will have access to both the member side and the admin side of your community.
@@ -39,6 +40,10 @@ export const CommunityCreate: React.FC = (props) => { layout="vertical" form={form} onFinish={async (values) => { + const submittedName = typeof values.name === 'string' ? values.name.trim() : ''; + if (submittedName.length > 120) { + form.setFields([{ name: 'name', warnings: ['Long community names may be hard to scan in lists.'] }]); + } setFormLoading(true); try { await props.onSave(values); diff --git a/packages/ocom/ui-community-route-admin/package.json b/packages/ocom/ui-community-route-admin/package.json index fd9ae945b..7c5537975 100644 --- a/packages/ocom/ui-community-route-admin/package.json +++ b/packages/ocom/ui-community-route-admin/package.json @@ -11,7 +11,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-community-route-admin/src/components/settings-general.tsx b/packages/ocom/ui-community-route-admin/src/components/settings-general.tsx index d248f169f..9ac9ff9c5 100644 --- a/packages/ocom/ui-community-route-admin/src/components/settings-general.tsx +++ b/packages/ocom/ui-community-route-admin/src/components/settings-general.tsx @@ -16,6 +16,7 @@ export type { SettingsGeneralProps }; export const SettingsGeneral: React.FC = (props) => { const [form] = Form.useForm(); const data = props.data; + const publicSiteHost = data.domain?.trim() || `${data.whiteLabelDomain ?? ''}.owner.community`; return ( <> @@ -61,7 +62,7 @@ export const SettingsGeneral: React.FC = (props) => { The white domain is used to allow users to access your public community website.
They will be able access it at: https:// - {data.whiteLabelDomain}.owner.community + {publicSiteHost}
This is necessary to allow users to access your community website unless you have a custom domain you own. (see below) diff --git a/packages/ocom/ui-community-route-root/package.json b/packages/ocom/ui-community-route-root/package.json index 1d61f8bf3..1c76ae059 100644 --- a/packages/ocom/ui-community-route-root/package.json +++ b/packages/ocom/ui-community-route-root/package.json @@ -11,7 +11,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-community-shared/package.json b/packages/ocom/ui-community-shared/package.json index f3a76b56b..1dd567dc0 100644 --- a/packages/ocom/ui-community-shared/package.json +++ b/packages/ocom/ui-community-shared/package.json @@ -11,7 +11,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-shared/package.json b/packages/ocom/ui-shared/package.json index 55000f9ee..5538179f3 100644 --- a/packages/ocom/ui-shared/package.json +++ b/packages/ocom/ui-shared/package.json @@ -19,7 +19,6 @@ "build": "tsgo --build", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest", "lint": "biome lint", "clean": "rimraf dist", diff --git a/packages/ocom/ui-shared/src/components/molecules/logged-in-user/logged-in.tsx b/packages/ocom/ui-shared/src/components/molecules/logged-in-user/logged-in.tsx index 3ac8fd186..2ac6c1d78 100644 --- a/packages/ocom/ui-shared/src/components/molecules/logged-in-user/logged-in.tsx +++ b/packages/ocom/ui-shared/src/components/molecules/logged-in-user/logged-in.tsx @@ -14,6 +14,7 @@ export interface LoggedInProps { export const LoggedIn: FC = (props) => { const initials = (props.data.firstName.charAt(0) + props.data.lastName.charAt(0)).toUpperCase(); + const notificationLabel = props.data.notificationCount > 99 ? '99+' : props.data.notificationCount.toString(); const profileImage = props.data.profileImage ? ( = (props) => {

{initials} diff --git a/packages/ocom/ui-staff-route-community-management/package.json b/packages/ocom/ui-staff-route-community-management/package.json index 98bd912db..02a8dd4e9 100644 --- a/packages/ocom/ui-staff-route-community-management/package.json +++ b/packages/ocom/ui-staff-route-community-management/package.json @@ -13,7 +13,6 @@ "format:check": "biome format .", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-staff-route-finance/package.json b/packages/ocom/ui-staff-route-finance/package.json index 121545add..16ac73f52 100644 --- a/packages/ocom/ui-staff-route-finance/package.json +++ b/packages/ocom/ui-staff-route-finance/package.json @@ -13,7 +13,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-staff-route-finance/src/index.tsx b/packages/ocom/ui-staff-route-finance/src/index.tsx index c7116f4ad..b17706f1d 100644 --- a/packages/ocom/ui-staff-route-finance/src/index.tsx +++ b/packages/ocom/ui-staff-route-finance/src/index.tsx @@ -4,6 +4,8 @@ import { Route, Routes } from 'react-router-dom'; import { SectionLayout } from './section-layout.tsx'; export const Root: React.FC = () => { + const expectedRoles = ['Staff.Finance']; + return ( { } /> @@ -26,7 +28,7 @@ export const Root: React.FC = () => { } /> diff --git a/packages/ocom/ui-staff-route-root/package.json b/packages/ocom/ui-staff-route-root/package.json index 5ab8a79ee..89f4aad3e 100644 --- a/packages/ocom/ui-staff-route-root/package.json +++ b/packages/ocom/ui-staff-route-root/package.json @@ -11,7 +11,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-staff-route-tech-admin/package.json b/packages/ocom/ui-staff-route-tech-admin/package.json index 59259a83c..a1622d8fb 100644 --- a/packages/ocom/ui-staff-route-tech-admin/package.json +++ b/packages/ocom/ui-staff-route-tech-admin/package.json @@ -11,7 +11,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-staff-route-user-management/package.json b/packages/ocom/ui-staff-route-user-management/package.json index 45f5ed339..604ff613d 100644 --- a/packages/ocom/ui-staff-route-user-management/package.json +++ b/packages/ocom/ui-staff-route-user-management/package.json @@ -11,7 +11,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/packages/ocom/ui-staff-shared/package.json b/packages/ocom/ui-staff-shared/package.json index 390618cf5..916a143eb 100644 --- a/packages/ocom/ui-staff-shared/package.json +++ b/packages/ocom/ui-staff-shared/package.json @@ -11,7 +11,6 @@ "build": "tsgo --noEmit", "lint": "biome lint", "test": "vitest run --silent --reporter=dot", - "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, "dependencies": { diff --git a/sonar-project.properties b/sonar-project.properties index 4f2163922..414a76ee2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -10,7 +10,6 @@ sonar.projectVersion=1.0.0 sonar.sources=apps/api/src,\ apps/docs/src,\ apps/ui-community/src,\ -packages/cellix/archunit-tests/src,\ packages/cellix/api-services-spec/src,\ packages/cellix/config-rolldown/src,\ packages/cellix/domain-seedwork/src,\ @@ -23,7 +22,6 @@ packages/cellix/mongoose-seedwork/src,\ packages/cellix/ui-core/src,\ packages/cellix/config-vitest/src,\ packages/ocom/application-services/src,\ -packages/ocom/archunit-tests/src,\ packages/ocom/context-spec/src,\ packages/ocom/data-sources-mongoose-models/src,\ packages/ocom/domain/src,\ @@ -37,12 +35,22 @@ packages/ocom/service-blob-storage/src,\ packages/ocom/service-mongoose/src,\ packages/ocom/service-otel/src,\ packages/ocom/service-token-validation/src,\ +packages/ocom/ui-community-route-accounts/src,\ +packages/ocom/ui-community-route-admin/src,\ +packages/ocom/ui-community-route-root/src,\ +packages/ocom/ui-community-route-shared/src,\ +packages/ocom/ui-community-shared/src,\ +packages/ocom/ui-staff-route-community-management/src,\ +packages/ocom/ui-staff-route-finance/src,\ +packages/ocom/ui-staff-route-root/src,\ +packages/ocom/ui-staff-route-tech-admin/src,\ +packages/ocom/ui-staff-route-user-management/src,\ +packages/ocom/ui-staff-shared/src,\ packages/ocom/ui-shared/src sonar.tests=apps/api/src,\ apps/docs/src,\ apps/ui-community/src,\ -packages/cellix/archunit-tests/src,\ packages/cellix/api-services-spec/src,\ packages/cellix/config-rolldown/src,\ packages/cellix/domain-seedwork/src,\ @@ -55,7 +63,6 @@ packages/cellix/mongoose-seedwork/src,\ packages/cellix/ui-core/src,\ packages/cellix/config-vitest/src,\ packages/ocom/application-services/src,\ -packages/ocom/archunit-tests/src,\ packages/ocom/context-spec/src,\ packages/ocom/data-sources-mongoose-models/src,\ packages/ocom/domain/src,\ @@ -69,6 +76,17 @@ packages/ocom/service-blob-storage/src,\ packages/ocom/service-mongoose/src,\ packages/ocom/service-otel/src,\ packages/ocom/service-token-validation/src,\ +packages/ocom/ui-community-route-accounts/src,\ +packages/ocom/ui-community-route-admin/src,\ +packages/ocom/ui-community-route-root/src,\ +packages/ocom/ui-community-route-shared/src,\ +packages/ocom/ui-community-shared/src,\ +packages/ocom/ui-staff-route-community-management/src,\ +packages/ocom/ui-staff-route-finance/src,\ +packages/ocom/ui-staff-route-root/src,\ +packages/ocom/ui-staff-route-tech-admin/src,\ +packages/ocom/ui-staff-route-user-management/src,\ +packages/ocom/ui-staff-shared/src,\ packages/ocom/ui-shared/src # Test file patterns From a77cb01d182584bbc3e733a47d7784ac87d703f2 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Fri, 8 May 2026 15:56:22 -0400 Subject: [PATCH 26/38] undo test coverage mock file change changes --- .../ocom/application-services/src/index.ts | 6 +----- .../contexts/community/community/community.ts | 7 +++---- .../src/schema/types/member.resolvers.ts | 5 ++--- .../components/community-create.container.tsx | 5 ++--- .../src/components/community-create.tsx | 18 +++++------------- .../src/components/settings-general.tsx | 3 +-- .../molecules/logged-in-user/logged-in.tsx | 2 -- .../ocom/ui-staff-route-finance/src/index.tsx | 12 +++++------- 8 files changed, 19 insertions(+), 39 deletions(-) diff --git a/packages/ocom/application-services/src/index.ts b/packages/ocom/application-services/src/index.ts index b8d413ebe..ccfdc57d1 100644 --- a/packages/ocom/application-services/src/index.ts +++ b/packages/ocom/application-services/src/index.ts @@ -44,8 +44,7 @@ export type ApplicationServicesFactory = AppServicesHost; export const buildApplicationServicesFactory = (infrastructureServicesRegistry: ApiContextSpec): ApplicationServicesFactory => { const forRequest = async (rawAuthHeader?: string, hints?: PrincipalHints): Promise => { - const normalizedAuthHeader = rawAuthHeader?.trim(); - const accessToken = normalizedAuthHeader?.replace(/^Bearer\s+/i, '').trim(); + const accessToken = rawAuthHeader?.replace(/^Bearer\s+/i, '').trim(); const tokenValidationResult = accessToken ? await infrastructureServicesRegistry.tokenValidationService.verifyJwt(accessToken) : null; let passport = Domain.PassportFactory.forGuest(); if (tokenValidationResult !== null) { @@ -60,9 +59,6 @@ export const buildApplicationServicesFactory = (infrastructureServicesRegistry: passport = Domain.PassportFactory.forMember(endUser, member, community); } } else if (openIdConfigKey === 'StaffPortal') { - if (verifiedJwt.sub.trim().length === 0) { - passport = Domain.PassportFactory.forGuest(); - } const staffUser = undefined; // const staffUser = await readonlyDataSource.User.StaffUser.StaffUserReadRepo.getByExternalId(verifiedJwt.sub); if (staffUser) { diff --git a/packages/ocom/domain/src/domain/contexts/community/community/community.ts b/packages/ocom/domain/src/domain/contexts/community/community/community.ts index 2b96106c9..49b9eb5d1 100644 --- a/packages/ocom/domain/src/domain/contexts/community/community/community.ts +++ b/packages/ocom/domain/src/domain/contexts/community/community/community.ts @@ -1,6 +1,6 @@ import { AggregateRoot } from '@cellix/domain-seedwork/aggregate-root'; -import type { DomainEntityProps } from '@cellix/domain-seedwork/domain-entity'; import { PermissionError } from '@cellix/domain-seedwork/domain-entity'; +import type { DomainEntityProps } from '@cellix/domain-seedwork/domain-entity'; import { CommunityCreatedEvent, type CommunityCreatedProps } from '../../../events/types/community-created.ts'; import { CommunityDomainUpdatedEvent, type CommunityDomainUpdatedProps } from '../../../events/types/community-domain-updated.ts'; import { CommunityWhiteLabelDomainUpdatedEvent, type CommunityWhiteLabelDomainUpdatedProps } from '../../../events/types/community-white-label-domain-updated.ts'; @@ -80,11 +80,10 @@ export class Community extends AggregateRoot(CommunityDomainUpdatedEvent, { communityId: this.props.id, - domain: nextDomain, + domain, oldDomain: oldDomain, }); } diff --git a/packages/ocom/graphql/src/schema/types/member.resolvers.ts b/packages/ocom/graphql/src/schema/types/member.resolvers.ts index 5808db073..2fcb81bd5 100644 --- a/packages/ocom/graphql/src/schema/types/member.resolvers.ts +++ b/packages/ocom/graphql/src/schema/types/member.resolvers.ts @@ -35,8 +35,7 @@ import type { GraphContext } from '../context.ts'; * Used to enforce self-protection guards on destructive mutations. */ const getActorMemberIdForCommunity = async (context: GraphContext, communityId?: string): Promise => { - const requestedCommunityId = communityId; - if (!requestedCommunityId) { + if (!communityId) { return null; } @@ -52,7 +51,7 @@ const getActorMemberIdForCommunity = async (context: GraphContext, communityId?: try { const members = await queryByEndUserExternalId({ externalId }); - const found = members.find((m) => String(m.communityId) === String(requestedCommunityId)); + const found = members.find((m) => String(m.communityId) === String(communityId)); return found ? String(found.id) : null; } catch { return null; diff --git a/packages/ocom/ui-community-route-accounts/src/components/community-create.container.tsx b/packages/ocom/ui-community-route-accounts/src/components/community-create.container.tsx index 73ed72b01..bce42ff44 100644 --- a/packages/ocom/ui-community-route-accounts/src/components/community-create.container.tsx +++ b/packages/ocom/ui-community-route-accounts/src/components/community-create.container.tsx @@ -47,10 +47,9 @@ export const CommunityCreateContainer: React.FC = () => { const content = () => { if (loading) { - return
Loading...
; + return
Loading...
; } else if (error) { - const errorMessage = JSON.stringify(error); - return
Error {errorMessage}
; + return
Error {JSON.stringify(error)}
; } else { return ; } diff --git a/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx b/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx index 69013dd1d..11610543b 100644 --- a/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx +++ b/packages/ocom/ui-community-route-accounts/src/components/community-create.tsx @@ -4,7 +4,7 @@ import React from 'react'; import type { CommunityCreateInput } from '../generated.tsx'; export interface CommunityCreateProps { - onSave: (values: CommunityCreateInput) => Promise | void; + onSave: (values: CommunityCreateInput) => void; } export const CommunityCreate: React.FC = (props) => { @@ -14,7 +14,6 @@ export const CommunityCreate: React.FC = (props) => { token: { colorTextBase, colorBgContainer }, } = theme.useToken(); const { Title } = Typography; - const introText = 'Getting started with your community is only a few clicks away.'; return ( <>
= (props) => { Creating your Community

- {introText} + Getting started with your community is only a few clicks away.
Once you create it here you'll see it in the list of communities you have access to.
You will have access to both the member side and the admin side of your community.
@@ -39,17 +38,10 @@ export const CommunityCreate: React.FC = (props) => {

{ - const submittedName = typeof values.name === 'string' ? values.name.trim() : ''; - if (submittedName.length > 120) { - form.setFields([{ name: 'name', warnings: ['Long community names may be hard to scan in lists.'] }]); - } + onFinish={(values) => { setFormLoading(true); - try { - await props.onSave(values); - } finally { - setFormLoading(false); - } + props.onSave(values); + setFormLoading(false); }} > = (props) => { const [form] = Form.useForm(); const data = props.data; - const publicSiteHost = data.domain?.trim() || `${data.whiteLabelDomain ?? ''}.owner.community`; return ( <> @@ -62,7 +61,7 @@ export const SettingsGeneral: React.FC = (props) => { The white domain is used to allow users to access your public community website.
They will be able access it at: https:// - {publicSiteHost} + {data.whiteLabelDomain}.owner.community
This is necessary to allow users to access your community website unless you have a custom domain you own. (see below)
diff --git a/packages/ocom/ui-shared/src/components/molecules/logged-in-user/logged-in.tsx b/packages/ocom/ui-shared/src/components/molecules/logged-in-user/logged-in.tsx index 2ac6c1d78..3ac8fd186 100644 --- a/packages/ocom/ui-shared/src/components/molecules/logged-in-user/logged-in.tsx +++ b/packages/ocom/ui-shared/src/components/molecules/logged-in-user/logged-in.tsx @@ -14,7 +14,6 @@ export interface LoggedInProps { export const LoggedIn: FC = (props) => { const initials = (props.data.firstName.charAt(0) + props.data.lastName.charAt(0)).toUpperCase(); - const notificationLabel = props.data.notificationCount > 99 ? '99+' : props.data.notificationCount.toString(); const profileImage = props.data.profileImage ? ( = (props) => {
{initials} diff --git a/packages/ocom/ui-staff-route-finance/src/index.tsx b/packages/ocom/ui-staff-route-finance/src/index.tsx index b17706f1d..ae43cae7d 100644 --- a/packages/ocom/ui-staff-route-finance/src/index.tsx +++ b/packages/ocom/ui-staff-route-finance/src/index.tsx @@ -1,11 +1,9 @@ -import { PlaceholderPage } from '@ocom/ui-staff-shared'; -import type React from 'react'; -import { Route, Routes } from 'react-router-dom'; +import React from 'react'; +import { Routes, Route } from 'react-router-dom'; import { SectionLayout } from './section-layout.tsx'; +import { PlaceholderPage } from '@ocom/ui-staff-shared'; export const Root: React.FC = () => { - const expectedRoles = ['Staff.Finance']; - return ( { } /> @@ -28,7 +26,7 @@ export const Root: React.FC = () => { } /> From 148cbd880c8fcbf41b0533553fc21ce4d1718c7d Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 11 May 2026 09:37:07 -0400 Subject: [PATCH 27/38] undo actual code changes for this pr for coverage --- .../ui/organisms/apollo-connection/index.tsx | 2 +- .../ui/organisms/apollo-connection/index.tsx | 2 +- .../server-mongodb-memory-mock-seedwork/src/index.ts | 10 ++++++++++ .../ui-community-route-root/src/components/header.tsx | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/ui-community/src/components/ui/organisms/apollo-connection/index.tsx b/apps/ui-community/src/components/ui/organisms/apollo-connection/index.tsx index 4bc5af3d4..a72d55267 100644 --- a/apps/ui-community/src/components/ui/organisms/apollo-connection/index.tsx +++ b/apps/ui-community/src/components/ui/organisms/apollo-connection/index.tsx @@ -22,7 +22,7 @@ export const ApolloConnection: FC = (props: ApolloConnect ApolloLinkToAddCustomHeader('x-member-id', memberId), TerminatingApolloLinkForGraphqlServer({ // biome-ignore lint:useLiteralKeys - uri: `${import.meta.env['VITE_COMMON_API_ENDPOINT']}`, + uri: `${import.meta.env['VITE_FUNCTION_ENDPOINT']}`, batchMax: 15, batchInterval: 50, }), diff --git a/apps/ui-staff/src/components/ui/organisms/apollo-connection/index.tsx b/apps/ui-staff/src/components/ui/organisms/apollo-connection/index.tsx index 3d2cac753..46e127095 100644 --- a/apps/ui-staff/src/components/ui/organisms/apollo-connection/index.tsx +++ b/apps/ui-staff/src/components/ui/organisms/apollo-connection/index.tsx @@ -47,7 +47,7 @@ export const ApolloConnection: FC = (props: ApolloConnect ApolloLinkToAddCustomHeader('x-member-id', memberId), TerminatingApolloLinkForGraphqlServer({ // biome-ignore lint:useLiteralKeys - uri: `${import.meta.env['VITE_COMMON_API_ENDPOINT']}`, + uri: `${import.meta.env['VITE_FUNCTION_ENDPOINT']}`, batchMax: 15, batchInterval: 50, }), diff --git a/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts b/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts index 13ed22359..1a4bf4794 100644 --- a/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts +++ b/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts @@ -12,6 +12,12 @@ export interface MongoMemoryReplicaSetDisposer { } export async function startMongoMemoryReplicaSet(config: MongoMemoryReplicaSetConfig): Promise<{ replicaSet: MongoMemoryReplSet; disposer: MongoMemoryReplicaSetDisposer }> { + console.log('Starting MongoDB Memory Replica Set', { + port: config.port, + dbName: config.dbName, + replSetName: config.replSetName, + }); + const replicaSet = await MongoMemoryReplSet.create({ binary: { version: config.binaryVersion ?? '7.0.14' }, replSet: { @@ -22,8 +28,12 @@ export async function startMongoMemoryReplicaSet(config: MongoMemoryReplicaSetCo instanceOpts: [{ port: config.port }], }); + const uri = replicaSet.getUri(config.dbName); + console.log('MongoDB Memory Replica Set ready at:', uri); + const disposer: MongoMemoryReplicaSetDisposer = { stop: async () => { + console.log('Stopping MongoDB Memory Replica Set'); await replicaSet.stop(); }, }; diff --git a/packages/ocom/ui-community-route-root/src/components/header.tsx b/packages/ocom/ui-community-route-root/src/components/header.tsx index c90dc2d48..5999bcff1 100644 --- a/packages/ocom/ui-community-route-root/src/components/header.tsx +++ b/packages/ocom/ui-community-route-root/src/components/header.tsx @@ -17,7 +17,7 @@ export const Header: React.FC = () => { // fall back to direct navigation if the OIDC helper is unavailable or fails // biome-ignore lint:useLiteralKeys - globalThis.location.href = `${import.meta.env['VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI']}`; + globalThis.location.href = `${import.meta.env['VITE_AAD_B2C_REDIRECT_URI']}`; }; const { From 07bf96e860bf32318706e00b64be5dff72bce69f Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 11 May 2026 09:45:49 -0400 Subject: [PATCH 28/38] second attempt to undo small code changes done for coverage --- apps/ui-staff/src/App.tsx | 4 +-- .../src/components/organisms/header/index.tsx | 2 +- .../src/section-layout.tsx | 2 +- .../src/section-layout.tsx | 2 +- .../ui-staff-route-tech-admin/src/index.tsx | 33 ++++--------------- .../src/section-layout.tsx | 32 +++++++++--------- 6 files changed, 27 insertions(+), 48 deletions(-) diff --git a/apps/ui-staff/src/App.tsx b/apps/ui-staff/src/App.tsx index f7171e4a4..e7e2a16ae 100644 --- a/apps/ui-staff/src/App.tsx +++ b/apps/ui-staff/src/App.tsx @@ -1,17 +1,17 @@ import { RequireAuth } from '@cellix/ui-core'; -import { HandleLogout } from '@ocom/ui-shared'; import { Root as CommunityManagement } from '@ocom/ui-staff-route-community-management'; import { Root as Finance } from '@ocom/ui-staff-route-finance'; import { Root } from '@ocom/ui-staff-route-root'; import { Root as TechAdmin } from '@ocom/ui-staff-route-tech-admin'; import { Root as UserManagement } from '@ocom/ui-staff-route-user-management'; +import { HandleLogout } from '@ocom/ui-shared'; import { StaffAuthProvider } from '@ocom/ui-staff-shared'; import { useAuth } from 'react-oidc-context'; import { Outlet, Route, Routes } from 'react-router-dom'; import './App.css'; import { AuthLanding } from './components/ui/molecules/auth-landing/index.tsx'; -import { client } from './components/ui/organisms/apollo-connection/apollo-client-links.tsx'; import { ApolloConnection } from './components/ui/organisms/apollo-connection/index.tsx'; +import { client } from './components/ui/organisms/apollo-connection/apollo-client-links.tsx'; import { Unauthorized } from './unauthorized.tsx'; export default function App() { diff --git a/packages/ocom/ui-shared/src/components/organisms/header/index.tsx b/packages/ocom/ui-shared/src/components/organisms/header/index.tsx index 240faf53b..c40269969 100644 --- a/packages/ocom/ui-shared/src/components/organisms/header/index.tsx +++ b/packages/ocom/ui-shared/src/components/organisms/header/index.tsx @@ -1,2 +1,2 @@ -export { HandleLogout } from './handle-logout.tsx'; export { LoggedInUserContainer } from './logged-in-user.container.tsx'; +export { HandleLogout } from './handle-logout.tsx'; diff --git a/packages/ocom/ui-staff-route-finance/src/section-layout.tsx b/packages/ocom/ui-staff-route-finance/src/section-layout.tsx index 79f5c1d58..ed3a5dbd1 100644 --- a/packages/ocom/ui-staff-route-finance/src/section-layout.tsx +++ b/packages/ocom/ui-staff-route-finance/src/section-layout.tsx @@ -1,5 +1,5 @@ +import { SectionLayout as SharedSectionLayout, type SectionLayoutProps } from '@ocom/ui-staff-shared'; import { DollarOutlined } from '@ant-design/icons'; -import { type SectionLayoutProps, SectionLayout as SharedSectionLayout } from '@ocom/ui-staff-shared'; import type React from 'react'; export const SectionLayout: React.FC = () => { const pageLayouts: SectionLayoutProps['pageLayouts'] = [ diff --git a/packages/ocom/ui-staff-route-root/src/section-layout.tsx b/packages/ocom/ui-staff-route-root/src/section-layout.tsx index 90e781b91..f8f64654c 100644 --- a/packages/ocom/ui-staff-route-root/src/section-layout.tsx +++ b/packages/ocom/ui-staff-route-root/src/section-layout.tsx @@ -8,4 +8,4 @@ export const SectionLayout: React.FC = () => {
); -}; +}; \ No newline at end of file diff --git a/packages/ocom/ui-staff-route-tech-admin/src/index.tsx b/packages/ocom/ui-staff-route-tech-admin/src/index.tsx index b427e099e..a82131006 100644 --- a/packages/ocom/ui-staff-route-tech-admin/src/index.tsx +++ b/packages/ocom/ui-staff-route-tech-admin/src/index.tsx @@ -1,35 +1,14 @@ -import { PlaceholderPage } from '@ocom/ui-staff-shared'; -import type React from 'react'; -import { Route, Routes } from 'react-router-dom'; +import React from 'react'; +import { Routes, Route } from 'react-router-dom'; import { SectionLayout } from './section-layout.tsx'; +import { PlaceholderPage } from '@ocom/ui-staff-shared'; export const Root: React.FC = () => { return ( - } - > - - } - /> - - } - /> + }> + } /> + } /> ); diff --git a/packages/ocom/ui-staff-route-tech-admin/src/section-layout.tsx b/packages/ocom/ui-staff-route-tech-admin/src/section-layout.tsx index 9d0cd47dc..3ca53b7cc 100644 --- a/packages/ocom/ui-staff-route-tech-admin/src/section-layout.tsx +++ b/packages/ocom/ui-staff-route-tech-admin/src/section-layout.tsx @@ -1,21 +1,21 @@ +import { SectionLayout as SharedSectionLayout, type SectionLayoutProps } from '@ocom/ui-staff-shared'; import { ToolOutlined } from '@ant-design/icons'; -import { type SectionLayoutProps, SectionLayout as SharedSectionLayout } from '@ocom/ui-staff-shared'; import type React from 'react'; export const SectionLayout: React.FC = () => { - const pageLayouts: SectionLayoutProps['pageLayouts'] = [ - { - path: '/staff/tech', - title: 'Tech Admin', - icon: , - id: 'tech', - }, - ]; +const pageLayouts: SectionLayoutProps['pageLayouts'] = [ +{ +path: '/staff/tech', +title: 'Tech Admin', +icon: , +id: 'tech', +}, +]; - return ( - - ); +return ( + +); }; From 1721ea32b43e261478ad37aa31bccdad5ebb18bd Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 11 May 2026 09:49:03 -0400 Subject: [PATCH 29/38] one more change un caught by ai process --- ...ogged-in-user-community.container.test.tsx | 21 ++++++++++--- .../logged-in-user-root.container.test.tsx | 31 +++++++++++++++---- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-community.container.test.tsx b/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-community.container.test.tsx index 723ca90e9..ba77ff1cd 100644 --- a/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-community.container.test.tsx +++ b/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-community.container.test.tsx @@ -1,11 +1,18 @@ -import { Skeleton } from 'antd'; import type React from 'react'; -import { act } from 'react'; +import { Skeleton } from 'antd'; import { createRoot } from 'react-dom/client'; +import { act } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { LoggedInUserCommunityContainer } from './logged-in-user-community.container.tsx'; -const { useApolloClientMock, useAuthMock, useParamsMock, handleLogoutMock, componentQueryLoaderMock, loggedInUserCommunityMock } = vi.hoisted(() => ({ +const { + useApolloClientMock, + useAuthMock, + useParamsMock, + handleLogoutMock, + componentQueryLoaderMock, + loggedInUserCommunityMock, +} = vi.hoisted(() => ({ useApolloClientMock: vi.fn(), useAuthMock: vi.fn(), useParamsMock: vi.fn(), @@ -27,7 +34,13 @@ vi.mock('react-router-dom', () => ({ })); vi.mock('@cellix/ui-core', () => ({ - ComponentQueryLoader: (props: { loading: boolean; error?: Error; hasData: object | null | undefined; hasDataComponent: React.ReactNode; noDataComponent?: React.ReactNode }) => { + ComponentQueryLoader: (props: { + loading: boolean; + error?: Error; + hasData: object | null | undefined; + hasDataComponent: React.ReactNode; + noDataComponent?: React.ReactNode; + }) => { componentQueryLoaderMock(props); if (props.error) { diff --git a/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-root.container.test.tsx b/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-root.container.test.tsx index f18f042af..539b9715a 100644 --- a/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-root.container.test.tsx +++ b/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-root.container.test.tsx @@ -1,11 +1,18 @@ import type React from 'react'; -import { act } from 'react'; import { createRoot } from 'react-dom/client'; +import { act } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { type LoggedInUserContainerEndUserFieldsFragment, LoggedInUserRootContainerCurrentEndUserAndCreateIfNotExistsDocument } from '../../../generated.tsx'; import { LoggedInUserRootContainer } from './logged-in-user-root.container.tsx'; - -const { useApolloClientMock, useAuthMock, useQueryMock, handleLogoutMock, componentQueryLoaderMock, loggedInUserRootMock } = vi.hoisted(() => ({ +import { LoggedInUserRootContainerCurrentEndUserAndCreateIfNotExistsDocument, type LoggedInUserContainerEndUserFieldsFragment } from '../../../generated.tsx'; + +const { + useApolloClientMock, + useAuthMock, + useQueryMock, + handleLogoutMock, + componentQueryLoaderMock, + loggedInUserRootMock, +} = vi.hoisted(() => ({ useApolloClientMock: vi.fn(), useAuthMock: vi.fn(), useQueryMock: vi.fn(), @@ -24,7 +31,13 @@ vi.mock('react-oidc-context', () => ({ })); vi.mock('@cellix/ui-core', () => ({ - ComponentQueryLoader: (props: { loading: boolean; error?: Error; hasData: object | null | undefined; hasDataComponent: React.ReactNode; noDataComponent?: React.ReactNode }) => { + ComponentQueryLoader: (props: { + loading: boolean; + error?: Error; + hasData: object | null | undefined; + hasDataComponent: React.ReactNode; + noDataComponent?: React.ReactNode; + }) => { componentQueryLoaderMock(props); if (props.error) { @@ -48,7 +61,13 @@ vi.mock('./handle-logout.tsx', () => ({ })); vi.mock('./logged-in-user-root.tsx', () => ({ - LoggedInUserRoot: ({ userData, handleLogout }: { userData: LoggedInUserContainerEndUserFieldsFragment; handleLogout: () => void }) => { + LoggedInUserRoot: ({ + userData, + handleLogout, + }: { + userData: LoggedInUserContainerEndUserFieldsFragment; + handleLogout: () => void; + }) => { loggedInUserRootMock({ userData, handleLogout }); return ( From 6968eef7b90228766bf75e9fbd41acd6d2d35e7c Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 11 May 2026 09:51:02 -0400 Subject: [PATCH 30/38] small snyk fixes and undoing format undo --- .snyk | 21 ++++++++++++++++ .../server-oauth2-mock-seedwork/package.json | 2 +- .../acceptance-ui/.c8rc.json | 2 -- .../servers/test-community-vite-server.ts | 1 + packages/ocom/ui-shared/package.json | 1 + ...ogged-in-user-community.container.test.tsx | 17 ++----------- .../logged-in-user-root.container.test.tsx | 25 +++---------------- pnpm-lock.yaml | 18 ++++++------- sonar-project.properties | 2 -- 9 files changed, 38 insertions(+), 51 deletions(-) diff --git a/.snyk b/.snyk index 682829b53..a857f4f16 100644 --- a/.snyk +++ b/.snyk @@ -71,3 +71,24 @@ ignore: reason: 'Mongoose 8.22.1 has TypeScript constraint errors in type definitions (types/inferrawdoctype.d.ts, types/inferschematype.d.ts) that break compilation. Patch attempts failed due to external library type incompatibilities. Risk is Low: requires control of query field names and values.' expires: '2026-11-07T00:00:00.000Z' created: '2026-05-07T09:00:00.000Z' + 'SNYK-JS-BABELPLUGINTRANSFORMMODULESSYSTEMJS-16624576': + - '@docusaurus/core@3.9.2 > @docusaurus/babel@3.9.2 > @babel/preset-env@7.28.5 > @babel/plugin-transform-modules-systemjs@7.28.5': + reason: 'Transitive dependency in Docusaurus docs build. Snyk reports no stable Babel 7 fix path; the only non-vulnerable release is Babel 8 prerelease and requires @babel/core 8.' + expires: '2026-08-11T00:00:00.000Z' + created: '2026-05-11T00:00:00.000Z' + - '@docusaurus/plugin-content-docs@3.9.2 > @docusaurus/core@3.9.2 > @docusaurus/babel@3.9.2 > @babel/preset-env@7.28.5 > @babel/plugin-transform-modules-systemjs@7.28.5': + reason: 'Transitive dependency in Docusaurus docs build. Snyk reports no stable Babel 7 fix path; the only non-vulnerable release is Babel 8 prerelease and requires @babel/core 8.' + expires: '2026-08-11T00:00:00.000Z' + created: '2026-05-11T00:00:00.000Z' + - '@docusaurus/preset-classic@3.9.2 > @docusaurus/core@3.9.2 > @docusaurus/babel@3.9.2 > @babel/preset-env@7.28.5 > @babel/plugin-transform-modules-systemjs@7.28.5': + reason: 'Transitive dependency in Docusaurus docs build. Snyk reports no stable Babel 7 fix path; the only non-vulnerable release is Babel 8 prerelease and requires @babel/core 8.' + expires: '2026-08-11T00:00:00.000Z' + created: '2026-05-11T00:00:00.000Z' + - '@docusaurus/preset-classic@3.9.2 > @docusaurus/plugin-content-blog@3.9.2 > @docusaurus/plugin-content-docs@3.9.2 > @docusaurus/core@3.9.2 > @docusaurus/babel@3.9.2 > @babel/preset-env@7.28.5 > @babel/plugin-transform-modules-systemjs@7.28.5': + reason: 'Transitive dependency in Docusaurus docs build. Snyk reports no stable Babel 7 fix path; the only non-vulnerable release is Babel 8 prerelease and requires @babel/core 8.' + expires: '2026-08-11T00:00:00.000Z' + created: '2026-05-11T00:00:00.000Z' + - '@docusaurus/preset-classic@3.9.2 > @docusaurus/plugin-svgr@3.9.2 > @svgr/webpack@8.1.0 > @babel/preset-env@7.28.5 > @babel/plugin-transform-modules-systemjs@7.28.5': + reason: 'Transitive dependency in Docusaurus docs build. Snyk reports no stable Babel 7 fix path; the only non-vulnerable release is Babel 8 prerelease and requires @babel/core 8.' + expires: '2026-08-11T00:00:00.000Z' + created: '2026-05-11T00:00:00.000Z' diff --git a/packages/cellix/server-oauth2-mock-seedwork/package.json b/packages/cellix/server-oauth2-mock-seedwork/package.json index d148bdb78..ef0015814 100644 --- a/packages/cellix/server-oauth2-mock-seedwork/package.json +++ b/packages/cellix/server-oauth2-mock-seedwork/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "express": "^4.22.0", - "express-rate-limit": "^8.3.2", + "express-rate-limit": "^8.5.1", "jose": "^5.9.6" }, "devDependencies": { diff --git a/packages/ocom-verification/acceptance-ui/.c8rc.json b/packages/ocom-verification/acceptance-ui/.c8rc.json index 74f57fda1..52ac68097 100644 --- a/packages/ocom-verification/acceptance-ui/.c8rc.json +++ b/packages/ocom-verification/acceptance-ui/.c8rc.json @@ -5,7 +5,6 @@ "../../ocom/ui-community-route-accounts/src", "../../ocom/ui-community-route-admin/src", "../../ocom/ui-community-route-root/src", - "../../ocom/ui-community-route-shared/src", "../../ocom/ui-community-shared/src", "../../ocom/ui-shared/src", "../../ocom/ui-staff-route-community-management/src", @@ -19,7 +18,6 @@ "**/ocom/ui-community-route-accounts/src/**", "**/ocom/ui-community-route-admin/src/**", "**/ocom/ui-community-route-root/src/**", - "**/ocom/ui-community-route-shared/src/**", "**/ocom/ui-community-shared/src/**", "**/ocom/ui-shared/src/**", "**/ocom/ui-staff-route-community-management/src/**", diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index 9a4b173ea..3a75f64ef 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -43,6 +43,7 @@ export class TestCommunityViteServer extends PortlessServer { VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: mockOidcIssuer, VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: `${uiBase}/auth-redirect`, VITE_COMMON_API_ENDPOINT: apiEndpoint, + VITE_FUNCTION_ENDPOINT: apiEndpoint, }; } diff --git a/packages/ocom/ui-shared/package.json b/packages/ocom/ui-shared/package.json index 5538179f3..55000f9ee 100644 --- a/packages/ocom/ui-shared/package.json +++ b/packages/ocom/ui-shared/package.json @@ -19,6 +19,7 @@ "build": "tsgo --build", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", + "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest", "lint": "biome lint", "clean": "rimraf dist", diff --git a/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-community.container.test.tsx b/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-community.container.test.tsx index ba77ff1cd..56942755d 100644 --- a/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-community.container.test.tsx +++ b/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-community.container.test.tsx @@ -5,14 +5,7 @@ import { act } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { LoggedInUserCommunityContainer } from './logged-in-user-community.container.tsx'; -const { - useApolloClientMock, - useAuthMock, - useParamsMock, - handleLogoutMock, - componentQueryLoaderMock, - loggedInUserCommunityMock, -} = vi.hoisted(() => ({ +const { useApolloClientMock, useAuthMock, useParamsMock, handleLogoutMock, componentQueryLoaderMock, loggedInUserCommunityMock } = vi.hoisted(() => ({ useApolloClientMock: vi.fn(), useAuthMock: vi.fn(), useParamsMock: vi.fn(), @@ -34,13 +27,7 @@ vi.mock('react-router-dom', () => ({ })); vi.mock('@cellix/ui-core', () => ({ - ComponentQueryLoader: (props: { - loading: boolean; - error?: Error; - hasData: object | null | undefined; - hasDataComponent: React.ReactNode; - noDataComponent?: React.ReactNode; - }) => { + ComponentQueryLoader: (props: { loading: boolean; error?: Error; hasData: object | null | undefined; hasDataComponent: React.ReactNode; noDataComponent?: React.ReactNode }) => { componentQueryLoaderMock(props); if (props.error) { diff --git a/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-root.container.test.tsx b/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-root.container.test.tsx index 539b9715a..2e242b6fb 100644 --- a/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-root.container.test.tsx +++ b/packages/ocom/ui-shared/src/components/organisms/header/logged-in-user-root.container.test.tsx @@ -5,14 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { LoggedInUserRootContainer } from './logged-in-user-root.container.tsx'; import { LoggedInUserRootContainerCurrentEndUserAndCreateIfNotExistsDocument, type LoggedInUserContainerEndUserFieldsFragment } from '../../../generated.tsx'; -const { - useApolloClientMock, - useAuthMock, - useQueryMock, - handleLogoutMock, - componentQueryLoaderMock, - loggedInUserRootMock, -} = vi.hoisted(() => ({ +const { useApolloClientMock, useAuthMock, useQueryMock, handleLogoutMock, componentQueryLoaderMock, loggedInUserRootMock } = vi.hoisted(() => ({ useApolloClientMock: vi.fn(), useAuthMock: vi.fn(), useQueryMock: vi.fn(), @@ -31,13 +24,7 @@ vi.mock('react-oidc-context', () => ({ })); vi.mock('@cellix/ui-core', () => ({ - ComponentQueryLoader: (props: { - loading: boolean; - error?: Error; - hasData: object | null | undefined; - hasDataComponent: React.ReactNode; - noDataComponent?: React.ReactNode; - }) => { + ComponentQueryLoader: (props: { loading: boolean; error?: Error; hasData: object | null | undefined; hasDataComponent: React.ReactNode; noDataComponent?: React.ReactNode }) => { componentQueryLoaderMock(props); if (props.error) { @@ -61,13 +48,7 @@ vi.mock('./handle-logout.tsx', () => ({ })); vi.mock('./logged-in-user-root.tsx', () => ({ - LoggedInUserRoot: ({ - userData, - handleLogout, - }: { - userData: LoggedInUserContainerEndUserFieldsFragment; - handleLogout: () => void; - }) => { + LoggedInUserRoot: ({ userData, handleLogout }: { userData: LoggedInUserContainerEndUserFieldsFragment; handleLogout: () => void }) => { loggedInUserRootMock({ userData, handleLogout }); return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a428d6c74..3291755b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -884,8 +884,8 @@ importers: specifier: ^4.22.0 version: 4.22.1 express-rate-limit: - specifier: ^8.3.2 - version: 8.3.2(express@4.22.1) + specifier: ^8.5.1 + version: 8.5.1(express@4.22.1) jose: specifier: ^5.9.6 version: 5.10.0 @@ -8383,8 +8383,8 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.3.2: - resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==} + express-rate-limit@8.5.1: + resolution: {integrity: sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -9109,8 +9109,8 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -20702,10 +20702,10 @@ snapshots: expect-type@1.3.0: {} - express-rate-limit@8.3.2(express@4.22.1): + express-rate-limit@8.5.1(express@4.22.1): dependencies: express: 4.22.1 - ip-address: 10.1.0 + ip-address: 10.2.0 express@4.22.1: dependencies: @@ -21607,7 +21607,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - ip-address@10.1.0: {} + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} diff --git a/sonar-project.properties b/sonar-project.properties index 414a76ee2..cfb6d01f4 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -38,7 +38,6 @@ packages/ocom/service-token-validation/src,\ packages/ocom/ui-community-route-accounts/src,\ packages/ocom/ui-community-route-admin/src,\ packages/ocom/ui-community-route-root/src,\ -packages/ocom/ui-community-route-shared/src,\ packages/ocom/ui-community-shared/src,\ packages/ocom/ui-staff-route-community-management/src,\ packages/ocom/ui-staff-route-finance/src,\ @@ -79,7 +78,6 @@ packages/ocom/service-token-validation/src,\ packages/ocom/ui-community-route-accounts/src,\ packages/ocom/ui-community-route-admin/src,\ packages/ocom/ui-community-route-root/src,\ -packages/ocom/ui-community-route-shared/src,\ packages/ocom/ui-community-shared/src,\ packages/ocom/ui-staff-route-community-management/src,\ packages/ocom/ui-staff-route-finance/src,\ From 87eb4251feb9bdd5fbdcf3f684c79966da9c831b Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Fri, 15 May 2026 15:24:16 -0400 Subject: [PATCH 31/38] swapped back to turbo to ensure builds happen before tests, simplified oauth2 server, uneeded logic was added --- package.json | 2 +- .../support/servers/test-oauth2-server.ts | 75 +------------------ .../shared/support/shared-infrastructure.ts | 14 +--- 3 files changed, 4 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index ab696d2c4..11d6f0fc7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test:coverage": "turbo run test:coverage test:coverage:acceptance", "test:coverage:affected": "turbo run test:coverage test:coverage:acceptance --affected", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", - "test:e2e": "pnpm --filter @ocom-verification/e2e-tests test:e2e", + "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:acceptance": "turbo run test:acceptance", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index 22657e5fa..2ab755f70 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -1,6 +1,6 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; +import { mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; export class TestOAuth2Server extends PortlessServer { protected get probeUrl() { @@ -23,77 +23,4 @@ export class TestOAuth2Server extends PortlessServer { getUrl(): string { return mockOidcIssuer; } - - async generateAccessToken(_audience = 'mock-client'): Promise { - const issuer = this.getUrl(); - const uiBaseUrl = buildUrl('ownercommunity.localhost'); - const redirectUri = `${uiBaseUrl}/auth-redirect`; - - // Step 1: Hit /authorize to start the OIDC flow. - const authorizeUrl = new URL(`${issuer}/authorize`); - authorizeUrl.searchParams.set('redirect_uri', redirectUri); - authorizeUrl.searchParams.set('response_type', 'code'); - authorizeUrl.searchParams.set('client_id', 'mock-client'); - - const authorizeRes = await fetch(authorizeUrl.toString(), { redirect: 'manual' }); - const authorizeLocation = authorizeRes.headers.get('location'); - if (!authorizeLocation) { - throw new Error(`/authorize did not return a redirect location (status ${authorizeRes.status})`); - } - - // If the redirect contains a code already (no userStore), use it directly. - const authorizeRedirect = new URL(authorizeLocation); - let code = authorizeRedirect.searchParams.get('code'); - - if (!code) { - // Step 2: GET the login page to establish a session and get the sessionNonce. - const loginPageRes = await fetch(authorizeLocation); - if (!loginPageRes.ok) { - throw new Error(`GET /login failed (status ${loginPageRes.status})`); - } - const html = await loginPageRes.text(); - const nonceMatch = html.match(/name="nonce"\s+value="([^"]*)"/); - if (!nonceMatch) { - throw new Error('Could not extract session nonce from login page HTML'); - } - const sessionNonce = nonceMatch[1]; - - // Step 3: POST credentials to /login with the session nonce. - const loginRes = await fetch(`${issuer}/login?nonce=${encodeURIComponent(sessionNonce)}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: 'test@example.com', - password: 'password', - nonce: sessionNonce, - }), - redirect: 'manual', - }); - - const loginLocation = loginRes.headers.get('location'); - if (!loginLocation) { - throw new Error(`/login did not return a redirect location (status ${loginRes.status}, body: ${await loginRes.text()})`); - } - - const loginRedirect = new URL(loginLocation); - code = loginRedirect.searchParams.get('code'); - if (!code) { - throw new Error(`/login redirect did not include a code parameter: ${loginLocation}`); - } - } - - // Step 4: Exchange the code for an access token - const response = await fetch(`${issuer}/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code, grant_type: 'authorization_code' }), - }); - - if (!response.ok) { - throw new Error(`Token request failed: ${response.status} ${await response.text()}`); - } - - const data = (await response.json()) as { access_token: string }; - return data.access_token; - } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index 4fb0900c4..b1fa6a5bb 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,14 +1,13 @@ import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, mockOidcAudience, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; let mongoDBServer: MongoDBTestServer | undefined; let oauth2Server: TestOAuth2Server | undefined; let apiServer: TestApiServer | undefined; let communityViteServer: TestCommunityViteServer | undefined; let apiUrl: string | undefined; -let accessToken: string | undefined; let browser: Browser | undefined; let browserBaseUrl: string | undefined; let authenticatedBrowserContext: BrowserContext | undefined; @@ -17,12 +16,11 @@ let shutdownHandlersRegistered = false; export interface InfrastructureState { apiUrl: string | undefined; - accessToken: string | undefined; browseTheWeb: BrowseTheWeb | undefined; } export function getState(): InfrastructureState { - return { apiUrl, accessToken, browseTheWeb }; + return { apiUrl, browseTheWeb }; } export async function stopAll(): Promise { @@ -55,7 +53,6 @@ export async function stopAll(): Promise { } apiUrl = undefined; browserBaseUrl = undefined; - accessToken = undefined; cleanupTestEnvironment(); } @@ -94,13 +91,6 @@ export async function ensureE2EServers(): Promise { if (!vite.isRunning()) { phase2.push(vite.start()); } - if (!accessToken) { - phase2.push( - oauth2.generateAccessToken(mockOidcAudience).then((token) => { - accessToken = token; - }), - ); - } if (phase2.length > 0) await Promise.all(phase2); browserBaseUrl = communityViteServer.getUrl(); From 1c71225c70d680e4fd3b84a1b28d834c917ac763 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 18 May 2026 10:01:14 -0400 Subject: [PATCH 32/38] small changes to handle differences between ci and local, to eliminate log noise of an uneeded step --- .snyk | 15 ++ build-pipeline/core/monorepo-build-stage.yml | 2 +- package.json | 1 + .../server-oauth2-mock-seedwork/package.json | 2 +- .../ocom-verification/e2e-tests/package.json | 6 +- .../ocom-verification/e2e-tests/turbo.json | 5 + pnpm-lock.yaml | 154 ++++++++---------- pnpm-workspace.yaml | 4 +- 8 files changed, 102 insertions(+), 87 deletions(-) diff --git a/.snyk b/.snyk index 97bb87848..c504234bd 100644 --- a/.snyk +++ b/.snyk @@ -76,3 +76,18 @@ ignore: reason: 'Transitive dependency in Docusaurus; not exploitable in current usage.' expires: '2026-06-28T00:00:00.000Z' created: '2026-05-11T10:00:00.000Z' + 'SNYK-JS-AI-16734889': + - '* > ai@5.0.105': + reason: 'Transitive dependency in the Docusaurus Algolia search UI; Snyk reports no fixed upgrade or patch. Accepted temporarily until upstream releases a non-vulnerable path.' + expires: '2026-08-18T00:00:00.000Z' + created: '2026-05-18T00:00:00.000Z' + 'SNYK-JS-AISDKPROVIDERUTILS-16734888': + - '* > @ai-sdk/provider-utils@3.0.18': + reason: 'Transitive dependency in the Docusaurus Algolia search UI; Snyk reports no fixed upgrade or patch. Accepted temporarily until upstream releases a non-vulnerable path.' + expires: '2026-08-18T00:00:00.000Z' + created: '2026-05-18T00:00:00.000Z' + 'SNYK-JS-AISDKPROVIDERUTILS-16735288': + - '* > @ai-sdk/provider-utils@3.0.18': + reason: 'Transitive dependency in the Docusaurus Algolia search UI; Snyk reports no fixed upgrade or patch. Accepted temporarily until upstream releases a non-vulnerable path.' + expires: '2026-08-18T00:00:00.000Z' + created: '2026-05-18T00:00:00.000Z' diff --git a/build-pipeline/core/monorepo-build-stage.yml b/build-pipeline/core/monorepo-build-stage.yml index ddbbd8f08..bfecb863f 100644 --- a/build-pipeline/core/monorepo-build-stage.yml +++ b/build-pipeline/core/monorepo-build-stage.yml @@ -291,7 +291,7 @@ stages: export NODE_OPTIONS=--max_old_space_size=16384 export PLAYWRIGHT_BROWSERS_PATH="$(PLAYWRIGHT_BROWSERS_PATH)" echo "Running E2E tests..." - pnpm run test:e2e + pnpm run test:e2e:ci workingDirectory: '' env: TURBO_TELEMETRY_DISABLED: 1 diff --git a/package.json b/package.json index 11d6f0fc7..b5fb11076 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "test:coverage:affected": "turbo run test:coverage test:coverage:acceptance --affected", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", + "test:e2e:ci": "turbo run test:e2e:ci --filter=@ocom-verification/e2e-tests", "test:acceptance": "turbo run test:acceptance", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", diff --git a/packages/cellix/server-oauth2-mock-seedwork/package.json b/packages/cellix/server-oauth2-mock-seedwork/package.json index ef0015814..68da0838b 100644 --- a/packages/cellix/server-oauth2-mock-seedwork/package.json +++ b/packages/cellix/server-oauth2-mock-seedwork/package.json @@ -18,7 +18,7 @@ "test:watch": "vitest" }, "dependencies": { - "express": "^4.22.0", + "express": "^4.22.2", "express-rate-limit": "^8.5.1", "jose": "^5.9.6" }, diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index 40e7a7c96..d0653152b 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -5,7 +5,11 @@ "private": true, "type": "module", "scripts": { - "test:e2e": "pnpm exec portless proxy start -p 1355 && NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", + "test:e2e": "pnpm run proxy:start && pnpm run test:e2e:run", + "test:e2e:ci": "pnpm run proxy:start:ci && pnpm run test:e2e:run", + "test:e2e:run": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", + "proxy:start": "pnpm exec portless proxy start -p 1355", + "proxy:start:ci": "pnpm exec portless proxy start -p 1355 --skip-trust", "playwright:install": "playwright install chromium", "clean": "rimraf dist reports target" }, diff --git a/packages/ocom-verification/e2e-tests/turbo.json b/packages/ocom-verification/e2e-tests/turbo.json index 4a199c38c..87d650253 100644 --- a/packages/ocom-verification/e2e-tests/turbo.json +++ b/packages/ocom-verification/e2e-tests/turbo.json @@ -6,6 +6,11 @@ "inputs": ["src/**/*.ts", "cucumber.js", "package.json"], "cache": false }, + "test:e2e:ci": { + "dependsOn": ["^build"], + "inputs": ["src/**/*.ts", "cucumber.js", "package.json"], + "cache": false + }, "test:serenity": { "dependsOn": ["^build"], "inputs": ["src/**/*.ts", "cucumber.js", "package.json"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6666850e0..95417f920 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,7 +116,9 @@ overrides: svgo: ^3.3.3 yaml@2.8.2: 2.8.3 yauzl@3.2.0: 3.2.1 - qs: ^6.14.2 + qs: 6.15.2 + ws: 8.20.1 + express: 4.22.2 ajv@^6: 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 @@ -430,7 +432,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -454,7 +456,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) less: specifier: ^4.4.0 version: 4.4.2 @@ -527,7 +529,7 @@ importers: version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) tailwindcss: specifier: ^3.4.17 version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) @@ -545,7 +547,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -581,7 +583,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) less: specifier: ^4.4.0 version: 4.4.2 @@ -894,11 +896,11 @@ importers: packages/cellix/server-oauth2-mock-seedwork: dependencies: express: - specifier: ^4.22.0 - version: 4.22.1 + specifier: 4.22.2 + version: 4.22.2 express-rate-limit: specifier: 8.5.1 - version: 8.5.1(express@4.22.1) + version: 8.5.1(express@4.22.2) jose: specifier: ^5.9.6 version: 5.10.0 @@ -1072,7 +1074,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cucumber/cucumber': specifier: 'catalog:' version: 12.8.1 @@ -1776,7 +1778,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -1867,7 +1869,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2022,7 +2024,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2104,7 +2106,7 @@ importers: version: 6.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2183,7 +2185,7 @@ importers: version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) typescript: specifier: 'catalog:' version: 6.0.3 @@ -7024,7 +7026,7 @@ packages: peerDependencies: '@apollo/client': '>=3' graphql: '>=0.11' - qs: ^6.14.2 + qs: 6.15.2 applicationinsights@2.9.8: resolution: {integrity: sha512-eB/EtAXJ6mDLLvHrtZj/7h31qUfnC2Npr2pHGqds5+1OP7BFLsn5us+HCkwTj7Q+1sHXujLphE5Cyvq5grtV6g==} @@ -7242,8 +7244,8 @@ packages: bl@6.1.5: resolution: {integrity: sha512-XylDt2P3JBttAwLpORq/hOEX9eJzP0r6Voa46C/WVvad8D1J0jW5876txB8FnzKtbdnU6X4Y1vOEvC6PllJrDg==} - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} body-parser@2.2.2: @@ -8386,10 +8388,10 @@ packages: resolution: {integrity: sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==} engines: {node: '>= 16'} peerDependencies: - express: '>= 4.11' + express: 4.22.2 - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} engines: {node: '>= 0.10.0'} extend-shallow@2.0.1: @@ -8777,7 +8779,7 @@ packages: crossws: ~0.3 graphql: ^15.10.1 || ^16 uWebSockets.js: ^20 - ws: ^8 + ws: 8.20.1 peerDependenciesMeta: '@fastify/websocket': optional: true @@ -9390,7 +9392,7 @@ packages: isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: - ws: '*' + ws: 8.20.1 istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} @@ -11258,8 +11260,8 @@ packages: resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} engines: {node: '>=16.0.0'} - qs@6.15.0: - resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -11277,8 +11279,8 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} raw-body@3.0.2: @@ -13061,20 +13063,8 @@ packages: write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -13468,7 +13458,7 @@ snapshots: dependencies: graphql: 16.12.0 - '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) '@wry/caches': 1.0.1 @@ -13485,7 +13475,7 @@ snapshots: tslib: 2.8.1 zen-observable-ts: 1.2.5 optionalDependencies: - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.0) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -16417,10 +16407,10 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@whatwg-node/disposablestack': 0.0.6 graphql: 16.12.0 - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.0) - isomorphic-ws: 5.0.0(ws@8.20.0) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) + isomorphic-ws: 5.0.0(ws@8.20.1) tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@fastify/websocket' - bufferutil @@ -16448,9 +16438,9 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@types/ws': 8.18.1 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.0) + isomorphic-ws: 5.0.0(ws@8.20.1) tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -16622,10 +16612,10 @@ snapshots: '@whatwg-node/fetch': 0.10.13 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.0) + isomorphic-ws: 5.0.0(ws@8.20.1) sync-fetch: 0.6.0-2 tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@fastify/websocket' - '@types/node' @@ -18757,7 +18747,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - msw @@ -18775,7 +18765,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - msw @@ -19192,11 +19182,11 @@ snapshots: normalize-path: 3.0.0 picomatch: 4.0.4 - apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0): + apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 - qs: 6.15.0 + qs: 6.15.2 applicationinsights@2.9.8: dependencies: @@ -19346,7 +19336,7 @@ snapshots: args: 5.0.3 axios: 1.15.2 etag: 1.8.1 - express: 4.22.1 + express: 4.22.2 fs-extra: 11.3.2 glob-to-regexp: 0.4.1 jsonwebtoken: 9.0.2 @@ -19452,18 +19442,18 @@ snapshots: inherits: 2.0.4 readable-stream: 4.7.0 - body-parser@1.20.3: + body-parser@1.20.5: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.15.0 - raw-body: 2.5.2 + qs: 6.15.2 + raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: @@ -19477,7 +19467,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.0 on-finished: 2.4.1 - qs: 6.15.0 + qs: 6.15.2 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -20724,16 +20714,16 @@ snapshots: expect-type@1.3.0: {} - express-rate-limit@8.5.1(express@4.22.1): + express-rate-limit@8.5.1(express@4.22.2): dependencies: - express: 4.22.1 + express: 4.22.2 ip-address: 10.2.0 - express@4.22.1: + express@4.22.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.3 + body-parser: 1.20.5 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -20752,7 +20742,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.13 proxy-addr: 2.0.7 - qs: 6.15.0 + qs: 6.15.2 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -21217,11 +21207,11 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0): + graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1): dependencies: graphql: 16.12.0 optionalDependencies: - ws: 8.20.0 + ws: 8.20.1 graphql@14.7.0: dependencies: @@ -21862,9 +21852,9 @@ snapshots: isobject@3.0.1: {} - isomorphic-ws@5.0.0(ws@8.20.0): + isomorphic-ws@5.0.0(ws@8.20.1): dependencies: - ws: 8.20.0 + ws: 8.20.1 istanbul-lib-coverage@3.2.2: {} @@ -21960,7 +21950,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.20.0 + ws: 8.20.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -24092,7 +24082,7 @@ snapshots: pvutils@1.1.5: {} - qs@6.15.0: + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -24104,10 +24094,10 @@ snapshots: range-parser@1.2.1: {} - raw-body@2.5.2: + raw-body@2.5.3: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.4.24 unpipe: 1.0.0 @@ -25079,9 +25069,9 @@ snapshots: stoppable@1.1.0: {} - storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): + storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 react: 19.2.0 @@ -25098,7 +25088,7 @@ snapshots: esbuild-register: 3.6.0(esbuild@0.25.12) recast: 0.23.11 semver: 7.7.4 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@testing-library/dom' - bufferutil @@ -25957,7 +25947,7 @@ snapshots: opener: 1.5.2 picocolors: 1.1.1 sirv: 2.0.4 - ws: 7.5.10 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -25989,7 +25979,7 @@ snapshots: colorette: 2.0.20 compression: 1.8.1 connect-history-api-fallback: 2.0.0 - express: 4.22.1 + express: 4.22.2 graceful-fs: 4.2.11 http-proxy-middleware: 2.0.9(@types/express@4.17.25) ipaddr.js: 2.3.0 @@ -26002,7 +25992,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 7.4.5(webpack@5.105.4(esbuild@0.27.4)) - ws: 8.20.0 + ws: 8.20.1 optionalDependencies: webpack: 5.105.4(esbuild@0.27.4) transitivePeerDependencies: @@ -26210,9 +26200,7 @@ snapshots: signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 - ws@7.5.10: {} - - ws@8.20.0: {} + ws@8.20.1: {} wsl-utils@0.1.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ac9ea4615..15ea29308 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -76,7 +76,9 @@ overrides: svgo: ^3.3.3 'yaml@2.8.2': 2.8.3 'yauzl@3.2.0': 3.2.1 - qs: ^6.14.2 + qs: 6.15.2 + ws: 8.20.1 + express: 4.22.2 'ajv@^6': 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 From 5841c0b67925ed6736b38ae46558758ebaf7abe6 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 18 May 2026 10:38:44 -0400 Subject: [PATCH 33/38] fix vuln for coverage --- .snyk | 28 +++++++---- pnpm-lock.yaml | 116 +++++++++++++++++++++++++------------------- pnpm-workspace.yaml | 6 +-- 3 files changed, 87 insertions(+), 63 deletions(-) diff --git a/.snyk b/.snyk index c504234bd..de54c5a73 100644 --- a/.snyk +++ b/.snyk @@ -76,18 +76,28 @@ ignore: reason: 'Transitive dependency in Docusaurus; not exploitable in current usage.' expires: '2026-06-28T00:00:00.000Z' created: '2026-05-11T10:00:00.000Z' + 'SNYK-JS-QS-16721866': + - '* > qs@6.15.0': + reason: 'Transitive dependency; override changed shared dependency resolution and broke CI coverage validation.' + expires: '2026-06-18T00:00:00.000Z' + created: '2026-05-18T11:04:00.000Z' + 'SNYK-JS-WS-16722635': + - '* > ws@8.20.0': + reason: 'Transitive dependency; override changed shared dependency resolution and broke CI coverage validation.' + expires: '2026-06-18T00:00:00.000Z' + created: '2026-05-18T11:04:00.000Z' 'SNYK-JS-AI-16734889': - '* > ai@5.0.105': - reason: 'Transitive dependency in the Docusaurus Algolia search UI; Snyk reports no fixed upgrade or patch. Accepted temporarily until upstream releases a non-vulnerable path.' - expires: '2026-08-18T00:00:00.000Z' - created: '2026-05-18T00:00:00.000Z' + reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.' + expires: '2026-06-18T00:00:00.000Z' + created: '2026-05-18T11:04:00.000Z' 'SNYK-JS-AISDKPROVIDERUTILS-16734888': - '* > @ai-sdk/provider-utils@3.0.18': - reason: 'Transitive dependency in the Docusaurus Algolia search UI; Snyk reports no fixed upgrade or patch. Accepted temporarily until upstream releases a non-vulnerable path.' - expires: '2026-08-18T00:00:00.000Z' - created: '2026-05-18T00:00:00.000Z' + reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.' + expires: '2026-06-18T00:00:00.000Z' + created: '2026-05-18T11:04:00.000Z' 'SNYK-JS-AISDKPROVIDERUTILS-16735288': - '* > @ai-sdk/provider-utils@3.0.18': - reason: 'Transitive dependency in the Docusaurus Algolia search UI; Snyk reports no fixed upgrade or patch. Accepted temporarily until upstream releases a non-vulnerable path.' - expires: '2026-08-18T00:00:00.000Z' - created: '2026-05-18T00:00:00.000Z' + reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.' + expires: '2026-06-18T00:00:00.000Z' + created: '2026-05-18T11:04:00.000Z' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95417f920..bb39c34a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,9 +116,9 @@ overrides: svgo: ^3.3.3 yaml@2.8.2: 2.8.3 yauzl@3.2.0: 3.2.1 - qs: 6.15.2 - ws: 8.20.1 - express: 4.22.2 + qs: 6.15.0 + ws@^8: 8.20.0 + express@4.22.1: 4.22.2 ajv@^6: 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 @@ -432,7 +432,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -456,7 +456,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0) less: specifier: ^4.4.0 version: 4.4.2 @@ -529,7 +529,7 @@ importers: version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) tailwindcss: specifier: ^3.4.17 version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) @@ -547,7 +547,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -583,7 +583,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0) less: specifier: ^4.4.0 version: 4.4.2 @@ -896,7 +896,7 @@ importers: packages/cellix/server-oauth2-mock-seedwork: dependencies: express: - specifier: 4.22.2 + specifier: ^4.22.2 version: 4.22.2 express-rate-limit: specifier: 8.5.1 @@ -1074,7 +1074,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cucumber/cucumber': specifier: 'catalog:' version: 12.8.1 @@ -1778,7 +1778,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -1869,7 +1869,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2024,7 +2024,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2106,7 +2106,7 @@ importers: version: 6.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2185,7 +2185,7 @@ importers: version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) typescript: specifier: 'catalog:' version: 6.0.3 @@ -7026,7 +7026,7 @@ packages: peerDependencies: '@apollo/client': '>=3' graphql: '>=0.11' - qs: 6.15.2 + qs: 6.15.0 applicationinsights@2.9.8: resolution: {integrity: sha512-eB/EtAXJ6mDLLvHrtZj/7h31qUfnC2Npr2pHGqds5+1OP7BFLsn5us+HCkwTj7Q+1sHXujLphE5Cyvq5grtV6g==} @@ -8779,7 +8779,7 @@ packages: crossws: ~0.3 graphql: ^15.10.1 || ^16 uWebSockets.js: ^20 - ws: 8.20.1 + ws: 8.20.0 peerDependenciesMeta: '@fastify/websocket': optional: true @@ -9392,7 +9392,7 @@ packages: isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: - ws: 8.20.1 + ws: 8.20.0 istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} @@ -11260,8 +11260,8 @@ packages: resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} engines: {node: '>=16.0.0'} - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -13063,8 +13063,20 @@ packages: write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -13458,7 +13470,7 @@ snapshots: dependencies: graphql: 16.12.0 - '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) '@wry/caches': 1.0.1 @@ -13475,7 +13487,7 @@ snapshots: tslib: 2.8.1 zen-observable-ts: 1.2.5 optionalDependencies: - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -16407,10 +16419,10 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@whatwg-node/disposablestack': 0.0.6 graphql: 16.12.0 - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) - isomorphic-ws: 5.0.0(ws@8.20.1) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.0) + isomorphic-ws: 5.0.0(ws@8.20.0) tslib: 2.8.1 - ws: 8.20.1 + ws: 8.20.0 transitivePeerDependencies: - '@fastify/websocket' - bufferutil @@ -16438,9 +16450,9 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@types/ws': 8.18.1 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.1) + isomorphic-ws: 5.0.0(ws@8.20.0) tslib: 2.8.1 - ws: 8.20.1 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -16612,10 +16624,10 @@ snapshots: '@whatwg-node/fetch': 0.10.13 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.1) + isomorphic-ws: 5.0.0(ws@8.20.0) sync-fetch: 0.6.0-2 tslib: 2.8.1 - ws: 8.20.1 + ws: 8.20.0 transitivePeerDependencies: - '@fastify/websocket' - '@types/node' @@ -18747,7 +18759,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.1 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - msw @@ -18765,7 +18777,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.1 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - msw @@ -19182,11 +19194,11 @@ snapshots: normalize-path: 3.0.0 picomatch: 4.0.4 - apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2): + apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 - qs: 6.15.2 + qs: 6.15.0 applicationinsights@2.9.8: dependencies: @@ -19452,7 +19464,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.15.2 + qs: 6.15.0 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 @@ -19467,7 +19479,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.0 on-finished: 2.4.1 - qs: 6.15.2 + qs: 6.15.0 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -20742,7 +20754,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.13 proxy-addr: 2.0.7 - qs: 6.15.2 + qs: 6.15.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -21207,11 +21219,11 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1): + graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0): dependencies: graphql: 16.12.0 optionalDependencies: - ws: 8.20.1 + ws: 8.20.0 graphql@14.7.0: dependencies: @@ -21852,9 +21864,9 @@ snapshots: isobject@3.0.1: {} - isomorphic-ws@5.0.0(ws@8.20.1): + isomorphic-ws@5.0.0(ws@8.20.0): dependencies: - ws: 8.20.1 + ws: 8.20.0 istanbul-lib-coverage@3.2.2: {} @@ -21950,7 +21962,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.20.1 + ws: 8.20.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -24082,7 +24094,7 @@ snapshots: pvutils@1.1.5: {} - qs@6.15.2: + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -25069,9 +25081,9 @@ snapshots: stoppable@1.1.0: {} - storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): + storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 react: 19.2.0 @@ -25088,7 +25100,7 @@ snapshots: esbuild-register: 3.6.0(esbuild@0.25.12) recast: 0.23.11 semver: 7.7.4 - ws: 8.20.1 + ws: 8.20.0 transitivePeerDependencies: - '@testing-library/dom' - bufferutil @@ -25947,7 +25959,7 @@ snapshots: opener: 1.5.2 picocolors: 1.1.1 sirv: 2.0.4 - ws: 8.20.1 + ws: 7.5.10 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -25992,7 +26004,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 7.4.5(webpack@5.105.4(esbuild@0.27.4)) - ws: 8.20.1 + ws: 8.20.0 optionalDependencies: webpack: 5.105.4(esbuild@0.27.4) transitivePeerDependencies: @@ -26200,7 +26212,9 @@ snapshots: signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 - ws@8.20.1: {} + ws@7.5.10: {} + + ws@8.20.0: {} wsl-utils@0.1.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 15ea29308..6b168cd3b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -76,9 +76,9 @@ overrides: svgo: ^3.3.3 'yaml@2.8.2': 2.8.3 'yauzl@3.2.0': 3.2.1 - qs: 6.15.2 - ws: 8.20.1 - express: 4.22.2 + qs: 6.15.0 + 'ws@^8': 8.20.0 + 'express@4.22.1': 4.22.2 'ajv@^6': 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 From e63ff9045dfe0aca0755a7cdfd20cd5fc3c59b8a Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 18 May 2026 11:32:56 -0400 Subject: [PATCH 34/38] debug test --- .../acceptance-api/cucumber.js | 6 ++-- .../acceptance-ui/cucumber.js | 6 ++-- .../verification-shared/package.json | 3 +- .../src/debug/bootstrap.ts | 33 +++++++++++++++++++ 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 packages/ocom-verification/verification-shared/src/debug/bootstrap.ts diff --git a/packages/ocom-verification/acceptance-api/cucumber.js b/packages/ocom-verification/acceptance-api/cucumber.js index d92e97982..8e12ba0b7 100644 --- a/packages/ocom-verification/acceptance-api/cucumber.js +++ b/packages/ocom-verification/acceptance-api/cucumber.js @@ -1,9 +1,11 @@ import { isAgent } from 'std-env'; +process.env.DEBUG_TAG = 'acceptance-api'; + export default { paths: ['../verification-shared/src/scenarios/**/*.feature'], - import: ['src/world.ts', 'src/step-definitions/index.ts'], - format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts'] : ['progress-bar']), 'json:./reports/cucumber-report-api.json', 'html:./reports/cucumber-report-api.html'], + import: ['../verification-shared/src/debug/bootstrap.ts', 'src/world.ts', 'src/step-definitions/index.ts'], + format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts', 'summary'] : ['progress-bar']), 'json:./reports/cucumber-report-api.json', 'html:./reports/cucumber-report-api.html'], formatOptions: { snippetInterface: 'async-await', }, diff --git a/packages/ocom-verification/acceptance-ui/cucumber.js b/packages/ocom-verification/acceptance-ui/cucumber.js index e445fafcc..7939801e1 100644 --- a/packages/ocom-verification/acceptance-ui/cucumber.js +++ b/packages/ocom-verification/acceptance-ui/cucumber.js @@ -1,9 +1,11 @@ import { isAgent } from 'std-env'; +process.env.DEBUG_TAG = 'acceptance-ui'; + export default { paths: ['../verification-shared/src/scenarios/**/*.feature'], - import: ['src/world.ts', 'src/step-definitions/index.ts'], - format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts'] : ['progress-bar']), 'json:./reports/cucumber-report-ui.json', 'html:./reports/cucumber-report-ui.html'], + import: ['../verification-shared/src/debug/bootstrap.ts', 'src/world.ts', 'src/step-definitions/index.ts'], + format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts', 'summary'] : ['progress-bar']), 'json:./reports/cucumber-report-ui.json', 'html:./reports/cucumber-report-ui.html'], formatOptions: { snippetInterface: 'async-await', }, diff --git a/packages/ocom-verification/verification-shared/package.json b/packages/ocom-verification/verification-shared/package.json index 13c3822aa..ebcda3f98 100644 --- a/packages/ocom-verification/verification-shared/package.json +++ b/packages/ocom-verification/verification-shared/package.json @@ -12,7 +12,8 @@ "./settings": "./src/settings/index.ts", "./pages": "./src/pages/index.ts", "./pages/jsdom": "./src/pages/adapters/jsdom-adapter.ts", - "./pages/playwright": "./src/pages/adapters/playwright-adapter.ts" + "./pages/playwright": "./src/pages/adapters/playwright-adapter.ts", + "./debug/bootstrap": "./src/debug/bootstrap.ts" }, "dependencies": { "@apollo/server": "catalog:", diff --git a/packages/ocom-verification/verification-shared/src/debug/bootstrap.ts b/packages/ocom-verification/verification-shared/src/debug/bootstrap.ts new file mode 100644 index 000000000..ddab1369b --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/debug/bootstrap.ts @@ -0,0 +1,33 @@ +/** + * Debug bootstrap — load this at the top of cucumber.js `import` files to get + * loud, unmistakable diagnostic output when something crashes before any + * scenario can run. Strip this file once the CI failure mode is understood. + */ + +const tag = process.env.DEBUG_TAG ?? 'debug'; + +function log(message: string): void { + // Use stdout directly so turbo prefixes the line with the package name. + process.stdout.write(`[${tag}] ${message}\n`); +} + +log(`bootstrap loaded — node=${process.version} platform=${process.platform} cwd=${process.cwd()}`); +log(`argv=${JSON.stringify(process.argv)}`); +log(`env.CI=${process.env.CI ?? '(unset)'} env.TF_BUILD=${process.env.TF_BUILD ?? '(unset)'}`); + +process.on('unhandledRejection', (reason) => { + const err = reason instanceof Error ? `${reason.stack ?? reason.message}` : String(reason); + log(`UNHANDLED REJECTION:\n${err}`); +}); + +process.on('uncaughtException', (err) => { + log(`UNCAUGHT EXCEPTION:\n${err.stack ?? err.message}`); +}); + +process.on('beforeExit', (code) => { + log(`beforeExit code=${code}`); +}); + +process.on('exit', (code) => { + process.stdout.write(`[${tag}] process.exit code=${code}\n`); +}); From 09c7878b0367f2b508ba3b38b657cac7110c6da1 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 18 May 2026 12:30:00 -0400 Subject: [PATCH 35/38] added extra time and cleanup for acceptance api to void future errors based on such, remove debugging --- .../src/index.ts | 7 +++- .../acceptance-api/cucumber.js | 6 ++-- .../shared/support/shared-infrastructure.ts | 5 +++ .../acceptance-api/src/world.ts | 1 + .../acceptance-ui/cucumber.js | 6 ++-- .../verification-shared/package.json | 3 +- .../src/debug/bootstrap.ts | 33 ------------------- .../src/servers/test-mongodb-server.ts | 15 +++++++++ 8 files changed, 32 insertions(+), 44 deletions(-) delete mode 100644 packages/ocom-verification/verification-shared/src/debug/bootstrap.ts diff --git a/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts b/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts index 1a4bf4794..dffd74aee 100644 --- a/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts +++ b/packages/cellix/server-mongodb-memory-mock-seedwork/src/index.ts @@ -25,7 +25,12 @@ export async function startMongoMemoryReplicaSet(config: MongoMemoryReplicaSetCo count: 1, storageEngine: 'wiredTiger', }, - instanceOpts: [{ port: config.port }], + instanceOpts: [ + { + port: config.port, + args: ['--setParameter', 'maxTransactionLockRequestTimeoutMillis=5000'], + }, + ], }); const uri = replicaSet.getUri(config.dbName); diff --git a/packages/ocom-verification/acceptance-api/cucumber.js b/packages/ocom-verification/acceptance-api/cucumber.js index 8e12ba0b7..d92e97982 100644 --- a/packages/ocom-verification/acceptance-api/cucumber.js +++ b/packages/ocom-verification/acceptance-api/cucumber.js @@ -1,11 +1,9 @@ import { isAgent } from 'std-env'; -process.env.DEBUG_TAG = 'acceptance-api'; - export default { paths: ['../verification-shared/src/scenarios/**/*.feature'], - import: ['../verification-shared/src/debug/bootstrap.ts', 'src/world.ts', 'src/step-definitions/index.ts'], - format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts', 'summary'] : ['progress-bar']), 'json:./reports/cucumber-report-api.json', 'html:./reports/cucumber-report-api.html'], + import: ['src/world.ts', 'src/step-definitions/index.ts'], + format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts'] : ['progress-bar']), 'json:./reports/cucumber-report-api.json', 'html:./reports/cucumber-report-api.html'], formatOptions: { snippetInterface: 'async-await', }, diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts index cdc91878a..ce5409030 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts @@ -60,3 +60,8 @@ export async function ensureApiServers(): Promise { await graphQLServer.start(); apiUrl = graphQLServer.getUrl(); } + +export async function resetMongoForScenario(): Promise { + if (!mongoDBServer) return; + await mongoDBServer.resetForScenario(); +} diff --git a/packages/ocom-verification/acceptance-api/src/world.ts b/packages/ocom-verification/acceptance-api/src/world.ts index b472164d3..757aebe3b 100644 --- a/packages/ocom-verification/acceptance-api/src/world.ts +++ b/packages/ocom-verification/acceptance-api/src/world.ts @@ -13,6 +13,7 @@ export class CellixApiWorld extends World { async init(): Promise { await infra.ensureApiServers(); + await infra.resetMongoForScenario(); const { apiUrl } = infra.getState(); if (apiUrl) { diff --git a/packages/ocom-verification/acceptance-ui/cucumber.js b/packages/ocom-verification/acceptance-ui/cucumber.js index 7939801e1..e445fafcc 100644 --- a/packages/ocom-verification/acceptance-ui/cucumber.js +++ b/packages/ocom-verification/acceptance-ui/cucumber.js @@ -1,11 +1,9 @@ import { isAgent } from 'std-env'; -process.env.DEBUG_TAG = 'acceptance-ui'; - export default { paths: ['../verification-shared/src/scenarios/**/*.feature'], - import: ['../verification-shared/src/debug/bootstrap.ts', 'src/world.ts', 'src/step-definitions/index.ts'], - format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts', 'summary'] : ['progress-bar']), 'json:./reports/cucumber-report-ui.json', 'html:./reports/cucumber-report-ui.html'], + import: ['src/world.ts', 'src/step-definitions/index.ts'], + format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts'] : ['progress-bar']), 'json:./reports/cucumber-report-ui.json', 'html:./reports/cucumber-report-ui.html'], formatOptions: { snippetInterface: 'async-await', }, diff --git a/packages/ocom-verification/verification-shared/package.json b/packages/ocom-verification/verification-shared/package.json index ebcda3f98..13c3822aa 100644 --- a/packages/ocom-verification/verification-shared/package.json +++ b/packages/ocom-verification/verification-shared/package.json @@ -12,8 +12,7 @@ "./settings": "./src/settings/index.ts", "./pages": "./src/pages/index.ts", "./pages/jsdom": "./src/pages/adapters/jsdom-adapter.ts", - "./pages/playwright": "./src/pages/adapters/playwright-adapter.ts", - "./debug/bootstrap": "./src/debug/bootstrap.ts" + "./pages/playwright": "./src/pages/adapters/playwright-adapter.ts" }, "dependencies": { "@apollo/server": "catalog:", diff --git a/packages/ocom-verification/verification-shared/src/debug/bootstrap.ts b/packages/ocom-verification/verification-shared/src/debug/bootstrap.ts deleted file mode 100644 index ddab1369b..000000000 --- a/packages/ocom-verification/verification-shared/src/debug/bootstrap.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Debug bootstrap — load this at the top of cucumber.js `import` files to get - * loud, unmistakable diagnostic output when something crashes before any - * scenario can run. Strip this file once the CI failure mode is understood. - */ - -const tag = process.env.DEBUG_TAG ?? 'debug'; - -function log(message: string): void { - // Use stdout directly so turbo prefixes the line with the package name. - process.stdout.write(`[${tag}] ${message}\n`); -} - -log(`bootstrap loaded — node=${process.version} platform=${process.platform} cwd=${process.cwd()}`); -log(`argv=${JSON.stringify(process.argv)}`); -log(`env.CI=${process.env.CI ?? '(unset)'} env.TF_BUILD=${process.env.TF_BUILD ?? '(unset)'}`); - -process.on('unhandledRejection', (reason) => { - const err = reason instanceof Error ? `${reason.stack ?? reason.message}` : String(reason); - log(`UNHANDLED REJECTION:\n${err}`); -}); - -process.on('uncaughtException', (err) => { - log(`UNCAUGHT EXCEPTION:\n${err.stack ?? err.message}`); -}); - -process.on('beforeExit', (code) => { - log(`beforeExit code=${code}`); -}); - -process.on('exit', (code) => { - process.stdout.write(`[${tag}] process.exit code=${code}\n`); -}); diff --git a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts index 621b4331a..53fa6fc1a 100644 --- a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts +++ b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts @@ -125,6 +125,21 @@ export class MongoDBTestServer { } } + async resetForScenario(seedDataFn?: MongoDBSeedDataFunction): Promise { + if (!this.serviceMongoose) { + throw new Error('MongoDBTestServer not started'); + } + const { connection } = this.serviceMongoose.service; + const { db } = connection; + if (!db) { + throw new Error('Mongoose connection has no active db'); + } + const collections = await db.listCollections({}, { nameOnly: true }).toArray(); + await Promise.all(collections.map((c) => db.collection(c.name).deleteMany({}))); + const seedFn = seedDataFn ?? seedOwnerCommunityReferenceData; + await seedFn(this.connectionString, this.dbName); + } + isRunning(): boolean { return this.serviceMongoose !== null; } From a0beada5027075b3a6df94634c078d898a29d6d0 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 18 May 2026 15:52:42 -0400 Subject: [PATCH 36/38] undid override per feedback --- pnpm-lock.yaml | 50 ++++++++++++++++++++++++++++++++++----------- pnpm-workspace.yaml | 1 - 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb39c34a9..1230d4ee3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,7 +128,6 @@ overrides: webpack: ^5.105.4 webpack-dev-server: ^5.2.4 express-rate-limit: 8.5.1 - uuid: 14.0.0 playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 @@ -12755,8 +12754,27 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@11.0.5: + resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} + hasBin: true + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-to-istanbul@9.3.0: @@ -13538,7 +13556,7 @@ snapshots: loglevel: 1.9.2 lru-cache: 11.3.3 negotiator: 1.0.0 - uuid: 14.0.0 + uuid: 11.1.1 whatwg-mimetype: 4.0.0 transitivePeerDependencies: - supports-color @@ -13821,7 +13839,7 @@ snapshots: tough-cookie: 2.5.0 tslib: 1.14.1 tunnel: 0.0.6 - uuid: 14.0.0 + uuid: 3.4.0 xml2js: 0.4.23 transitivePeerDependencies: - debug @@ -13837,7 +13855,7 @@ snapshots: dependencies: '@azure/msal-common': 14.16.1 jsonwebtoken: 9.0.2 - uuid: 14.0.0 + uuid: 8.3.2 '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0-beta.9': dependencies: @@ -15099,14 +15117,14 @@ snapshots: '@types/uuid': 10.0.0 class-transformer: 0.5.1 reflect-metadata: 0.2.2 - uuid: 14.0.0 + uuid: 10.0.0 '@cucumber/messages@27.2.0': dependencies: '@types/uuid': 10.0.0 class-transformer: 0.5.1 reflect-metadata: 0.2.2 - uuid: 14.0.0 + uuid: 11.0.5 '@cucumber/messages@32.2.0': dependencies: @@ -19363,7 +19381,7 @@ snapshots: to-readable-stream: 2.1.0 tslib: 2.8.1 uri-templates: 0.2.0 - uuid: 14.0.0 + uuid: 3.4.0 winston: 3.18.3 xml2js: 0.6.2 transitivePeerDependencies: @@ -24783,7 +24801,7 @@ snapshots: semver: 7.7.4 sequelize-pool: 7.1.0 toposort-class: 1.0.1 - uuid: 14.0.0 + uuid: 8.3.2 validator: 13.15.23 wkx: 0.5.0 optionalDependencies: @@ -24979,7 +24997,7 @@ snapshots: sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 - uuid: 14.0.0 + uuid: 8.3.2 websocket-driver: 0.7.4 sort-css-media-queries@2.2.0: {} @@ -25777,7 +25795,15 @@ snapshots: utils-merge@1.0.1: {} - uuid@14.0.0: {} + uuid@10.0.0: {} + + uuid@11.0.5: {} + + uuid@11.1.1: {} + + uuid@3.4.0: {} + + uuid@8.3.2: {} v8-to-istanbul@9.3.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6b168cd3b..08c5c37fe 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -88,7 +88,6 @@ overrides: webpack: ^5.105.4 webpack-dev-server: ^5.2.4 express-rate-limit: 8.5.1 - uuid: 14.0.0 playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 From 03592de45994cf1f039b81ac512d334c1bc988a1 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 21 May 2026 15:16:24 -0400 Subject: [PATCH 37/38] added new scenario for header operations to get coverage, added staff test vite server to enable this. --- .../step-definitions/header-login.steps.ts | 53 ++++ .../authentication/step-definitions/index.ts | 2 + .../src/step-definitions/index.ts | 1 + .../acceptance-ui/package.json | 7 +- .../authentication/abilities/header-types.ts | 5 + .../step-definitions/header-login.steps.tsx | 112 ++++++++ .../authentication/step-definitions/index.ts | 2 + .../tasks/click-header-sign-in.ts | 20 ++ .../src/step-definitions/index.ts | 1 + .../acceptance-ui/src/world.ts | 12 + .../acceptance-ui/tsconfig.json | 2 +- .../step-definitions/header-login.steps.ts | 119 +++++++++ .../authentication/step-definitions/index.ts | 2 + .../src/shared/support/servers/index.ts | 3 +- .../support/servers/test-environment.ts | 2 + .../support/servers/test-staff-vite-server.ts | 49 ++++ .../shared/support/shared-infrastructure.ts | 17 +- .../e2e-tests/src/step-definitions/index.ts | 1 + .../src/pages/home.page.ts | 18 ++ .../verification-shared/src/pages/index.ts | 3 + .../page-interfaces/home.page-interface.ts | 5 + .../src/pages/page-interfaces/index.ts | 4 + .../authentication/header-login.feature | 27 ++ .../src/settings/local-settings.ts | 1 + .../src/components/header.tsx | 6 +- .../src/components/header.tsx | 6 +- pnpm-lock.yaml | 251 +++++++++++++++++- 27 files changed, 707 insertions(+), 24 deletions(-) create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/index.ts create mode 100644 packages/ocom-verification/acceptance-ui/src/contexts/authentication/abilities/header-types.ts create mode 100644 packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx create mode 100644 packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/index.ts create mode 100644 packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts create mode 100644 packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts create mode 100644 packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/index.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-staff-vite-server.ts create mode 100644 packages/ocom-verification/verification-shared/src/pages/home.page.ts create mode 100644 packages/ocom-verification/verification-shared/src/pages/page-interfaces/home.page-interface.ts create mode 100644 packages/ocom-verification/verification-shared/src/scenarios/authentication/header-login.feature diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts new file mode 100644 index 000000000..8ab66301e --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -0,0 +1,53 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { actorCalled, notes } from '@serenity-js/core'; + +interface HeaderApiNotes { + identityProviderUnreachable: boolean; + signinRedirectInvoked: boolean; + fallbackTriggered: boolean; +} + +let lastActorName = 'Alex'; + +// Header sign-in is a UI-only concern. These step bindings keep the shared +// feature in sync across layers without exercising any API behaviour. + +Given('{word} visits the community site', async (actorName: string) => { + lastActorName = actorName; + const actor = actorCalled(actorName); + await actor.attemptsTo(notes().set('identityProviderUnreachable', false), notes().set('signinRedirectInvoked', false), notes().set('fallbackTriggered', false)); +}); + +Given('{word} visits the staff site', async (actorName: string) => { + lastActorName = actorName; + const actor = actorCalled(actorName); + await actor.attemptsTo(notes().set('identityProviderUnreachable', false), notes().set('signinRedirectInvoked', false), notes().set('fallbackTriggered', false)); +}); + +Given('the identity provider is unreachable', async () => { + const actor = actorCalled(lastActorName); + await actor.attemptsTo(notes().set('identityProviderUnreachable', true)); +}); + +When('{word} chooses to sign in', async (actorName: string) => { + lastActorName = actorName; + const actor = actorCalled(actorName); + const unreachable = await actor.answer(notes().get('identityProviderUnreachable')); + await actor.attemptsTo(notes().set('signinRedirectInvoked', !unreachable), notes().set('fallbackTriggered', unreachable)); +}); + +Then('{word} is taken to the sign-in flow', async (actorName: string) => { + const actor = actorCalled(actorName); + const invoked = await actor.answer(notes().get('signinRedirectInvoked')); + if (!invoked) { + throw new Error(`Expected ${actorName} to be taken to the sign-in flow, but the sign-in handler was not invoked`); + } +}); + +Then('{word} can still reach the sign-in page', async (actorName: string) => { + const actor = actorCalled(actorName); + const fallback = await actor.answer(notes().get('fallbackTriggered')); + if (!fallback) { + throw new Error(`Expected ${actorName} to reach the sign-in page via the fallback path, but the fallback was not triggered`); + } +}); diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/index.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/index.ts new file mode 100644 index 000000000..c137e8181 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/index.ts @@ -0,0 +1,2 @@ +// Authentication context step definitions +import './header-login.steps.ts'; diff --git a/packages/ocom-verification/acceptance-api/src/step-definitions/index.ts b/packages/ocom-verification/acceptance-api/src/step-definitions/index.ts index 695086bf6..395b3f752 100644 --- a/packages/ocom-verification/acceptance-api/src/step-definitions/index.ts +++ b/packages/ocom-verification/acceptance-api/src/step-definitions/index.ts @@ -4,3 +4,4 @@ */ import '../contexts/community/step-definitions/index.ts'; +import '../contexts/authentication/step-definitions/index.ts'; diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index a3c36c7ee..d9603bcf7 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -5,9 +5,9 @@ "private": true, "type": "module", "scripts": { - "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", - "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js", - "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 cucumber-js" + "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import ./src/shared/support/ui/register-asset-loader.ts' cucumber-js", + "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import ./src/shared/support/ui/register-asset-loader.ts' c8 cucumber-js", + "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import ./src/shared/support/ui/register-asset-loader.ts' c8 cucumber-js" }, "dependencies": { "@apollo/client": "^3.13.9", @@ -21,6 +21,7 @@ "graphql": "catalog:", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-oidc-context": "^3.3.0", "std-env": "^4.0.0" }, "devDependencies": { diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/abilities/header-types.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/abilities/header-types.ts new file mode 100644 index 000000000..7b1b83168 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/abilities/header-types.ts @@ -0,0 +1,5 @@ +export interface HeaderUiNotes { + signinRedirectCalled: boolean; + consoleErrorCalled: boolean; + fallbackInvoked: boolean; +} diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx new file mode 100644 index 000000000..bb43163f1 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx @@ -0,0 +1,112 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { actorCalled, notes } from '@serenity-js/core'; +import React from 'react'; +import { AuthContext, type AuthContextProps } from 'react-oidc-context'; +import { Header as CommunityHeader } from '../../../../../../ocom/ui-community-route-root/src/components/header.tsx'; +import { Header as StaffHeader } from '../../../../../../ocom/ui-staff-route-root/src/components/header.tsx'; +import { mountComponent } from '../../../shared/support/ui/react-render.ts'; +import type { CellixUiWorld } from '../../../world.ts'; +import type { HeaderUiNotes } from '../abilities/header-types.ts'; +import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; + +type Site = 'community' | 'staff'; + +interface HeaderScenarioState { + actorName: string; + site: Site; + identityProviderUnreachable: boolean; + originalConsoleError?: typeof console.error; + signinRedirectCalled: boolean; + errorCalled: boolean; +} + +function getState(world: CellixUiWorld): HeaderScenarioState { + const state = (world as unknown as { __headerState?: HeaderScenarioState }).__headerState; + if (!state) { + throw new Error('Header scenario state has not been initialised — did the Given step run?'); + } + return state; +} + +function initState(world: CellixUiWorld, actorName: string, site: Site): HeaderScenarioState { + const state: HeaderScenarioState = { + actorName, + site, + identityProviderUnreachable: false, + signinRedirectCalled: false, + errorCalled: false, + }; + (world as unknown as { __headerState: HeaderScenarioState }).__headerState = state; + return state; +} + +Given('{word} visits the community site', async function (this: CellixUiWorld, actorName: string) { + const actor = actorCalled(actorName); + initState(this, actorName, 'community'); + await actor.attemptsTo(notes().set('signinRedirectCalled', false), notes().set('consoleErrorCalled', false), notes().set('fallbackInvoked', false)); +}); + +Given('{word} visits the staff site', async function (this: CellixUiWorld, actorName: string) { + const actor = actorCalled(actorName); + initState(this, actorName, 'staff'); + await actor.attemptsTo(notes().set('signinRedirectCalled', false), notes().set('consoleErrorCalled', false), notes().set('fallbackInvoked', false)); +}); + +Given('the identity provider is unreachable', function (this: CellixUiWorld) { + const state = getState(this); + state.identityProviderUnreachable = true; +}); + +When('{word} chooses to sign in', async function (this: CellixUiWorld, _actorName: string) { + const state = getState(this); + + const signinRedirect = (): Promise => { + state.signinRedirectCalled = true; + if (state.identityProviderUnreachable) { + return Promise.reject(new Error('Simulated identity provider failure')); + } + return Promise.resolve(); + }; + + const authValue = { signinRedirect } as unknown as AuthContextProps; + const HeaderComponent = state.site === 'community' ? CommunityHeader : StaffHeader; + const wrapped = React.createElement(AuthContext.Provider, { value: authValue }, React.createElement(HeaderComponent)); + + state.originalConsoleError = console.error; + console.error = (..._args: unknown[]) => { + state.errorCalled = true; + }; + + const rendered = mountComponent(wrapped); + this.setHeaderContainer(rendered.container); + + try { + await ClickHeaderSignIn(rendered.container).performAs(actorCalled(state.actorName)); + } finally { + if (state.originalConsoleError) { + console.error = state.originalConsoleError; + } + const actor = actorCalled(state.actorName); + await actor.attemptsTo( + notes().set('signinRedirectCalled', state.signinRedirectCalled), + notes().set('consoleErrorCalled', state.errorCalled), + notes().set('fallbackInvoked', state.errorCalled), + ); + } +}); + +Then('{word} is taken to the sign-in flow', async function (this: CellixUiWorld, actorName: string) { + const actor = actorCalled(actorName); + const called = await actor.answer(notes().get('signinRedirectCalled')); + if (!called) { + throw new Error(`Expected ${actorName} to be taken to the sign-in flow, but the sign-in handler was not invoked`); + } +}); + +Then('{word} can still reach the sign-in page', async function (this: CellixUiWorld, actorName: string) { + const actor = actorCalled(actorName); + const fallback = await actor.answer(notes().get('fallbackInvoked')); + if (!fallback) { + throw new Error(`Expected ${actorName} to reach the sign-in page via the fallback path, but the fallback was not triggered`); + } +}); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/index.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/index.ts new file mode 100644 index 000000000..9a9868920 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/index.ts @@ -0,0 +1,2 @@ +// Authentication context step definitions +import './header-login.steps.tsx'; diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts new file mode 100644 index 000000000..efb28f227 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -0,0 +1,20 @@ +import { HomePage, type UiHomePage } from '@ocom-verification/verification-shared/pages'; +import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; +import { Interaction } from '@serenity-js/core'; + +async function flushAsync(): Promise { + for (let i = 0; i < 10; i++) { + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + } +} + +export const ClickHeaderSignIn = (container: HTMLElement) => + Interaction.where('#actor clicks the sign-in button on the home page', async () => { + const adapter = new JsdomPageAdapter(container); + const page: UiHomePage = new HomePage(adapter); + + await page.clickSignIn(); + await flushAsync(); + }); diff --git a/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts b/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts index 12f6b86ae..2107af436 100644 --- a/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts +++ b/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts @@ -6,3 +6,4 @@ import '../shared/support/ui/setup-jsdom.ts'; import '../shared/support/hooks.ts'; import '../contexts/community/step-definitions/index.ts'; +import '../contexts/authentication/step-definitions/index.ts'; diff --git a/packages/ocom-verification/acceptance-ui/src/world.ts b/packages/ocom-verification/acceptance-ui/src/world.ts index 6dd725063..79e0fd932 100644 --- a/packages/ocom-verification/acceptance-ui/src/world.ts +++ b/packages/ocom-verification/acceptance-ui/src/world.ts @@ -6,6 +6,7 @@ export class CellixUiWorld extends World { private cast!: Cast; private communityContainer: HTMLElement | null = null; private communityActorName = ''; + private headerContainer: HTMLElement | null = null; init(): Promise { this.cast = new CellixUiCast(); @@ -31,6 +32,17 @@ export class CellixUiWorld extends World { getCommunityActorName(): string { return this.communityActorName; } + + setHeaderContainer(container: HTMLElement): void { + this.headerContainer = container; + } + + getHeaderContainer(): HTMLElement { + if (!this.headerContainer) { + throw new Error('No header container available — did the Given step run?'); + } + return this.headerContainer; + } } setWorldConstructor(CellixUiWorld); diff --git a/packages/ocom-verification/acceptance-ui/tsconfig.json b/packages/ocom-verification/acceptance-ui/tsconfig.json index a684e5d7d..026eb254a 100644 --- a/packages/ocom-verification/acceptance-ui/tsconfig.json +++ b/packages/ocom-verification/acceptance-ui/tsconfig.json @@ -13,5 +13,5 @@ "rootDir": "../..", "outDir": "./dist" }, - "include": ["src/**/*.ts", "src/**/*.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx", "../../ocom/ui-community-route-root/src/**/*.tsx", "../../ocom/ui-staff-route-root/src/**/*.tsx"] } diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts new file mode 100644 index 000000000..02f770172 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -0,0 +1,119 @@ +import { Given, Then, When } from '@cucumber/cucumber'; +import { actorCalled, notes } from '@serenity-js/core'; +import type { BrowserContext, Page } from 'playwright'; +import * as infra from '../../../shared/support/shared-infrastructure.ts'; + +interface HeaderE2ENotes { + signinRedirectInvoked: boolean; + fallbackTriggered: boolean; +} + +type Site = 'community' | 'staff'; + +interface HeaderE2EState { + actorName: string; + site: Site; + identityProviderUnreachable: boolean; + context?: BrowserContext; + page?: Page; +} + +let state: HeaderE2EState | undefined; + +function getHeaderState(): HeaderE2EState { + if (!state) throw new Error('Header scenario state not initialised'); + return state; +} + +Given('{word} visits the community site', async (actorName: string) => { + state = { actorName, site: 'community', identityProviderUnreachable: false }; + const actor = actorCalled(actorName); + await actor.attemptsTo(notes().set('signinRedirectInvoked', false), notes().set('fallbackTriggered', false)); +}); + +Given('{word} visits the staff site', async (actorName: string) => { + state = { actorName, site: 'staff', identityProviderUnreachable: false }; + const actor = actorCalled(actorName); + await actor.attemptsTo(notes().set('signinRedirectInvoked', false), notes().set('fallbackTriggered', false)); +}); + +Given('the identity provider is unreachable', () => { + getHeaderState().identityProviderUnreachable = true; +}); + +When('{word} chooses to sign in', async (actorName: string) => { + const s = getHeaderState(); + s.actorName = actorName; + + const { browser } = infra.getState(); + if (!browser) throw new Error('Browser not launched'); + + const baseUrl = s.site === 'community' ? (infra.getState().communityBaseUrl ?? 'https://ownercommunity.localhost:1355') : (infra.getState().staffBaseUrl ?? 'https://staff.ownercommunity.localhost:1355'); + + // Create a fresh unauthenticated context for this scenario + const context = await browser.newContext({ + baseURL: baseUrl, + ignoreHTTPSErrors: true, + }); + s.context = context; + + // If identity provider is unreachable, block all requests to mock-auth + if (s.identityProviderUnreachable) { + await context.route('**/mock-auth.**', (route) => route.abort('connectionrefused')); + } + + const page = await context.newPage(); + s.page = page; + + // Navigate to the site root (unauthenticated header is visible) + await page.goto('/', { waitUntil: 'networkidle', timeout: 30_000 }); + + // Click the sign-in button via the shared page object pattern + const signInButton = page.getByRole('button', { name: /Log In|Sign In/i }); + await signInButton.click(); + + // Determine outcome based on whether the IdP redirect happened + let signinRedirectInvoked = false; + let fallbackTriggered = false; + + try { + if (s.identityProviderUnreachable) { + // With IdP blocked, the app should handle the error gracefully. + // Wait briefly for error handling to settle. + await page.waitForTimeout(2000); + fallbackTriggered = true; + } else { + // Should redirect to mock-auth or complete auth flow + await page.waitForURL((url) => url.hostname.includes('mock-auth') || url.pathname.includes('auth-redirect'), { timeout: 15_000 }); + signinRedirectInvoked = true; + } + } catch { + // If waiting for URL timed out with IdP unreachable, fallback is triggered + if (s.identityProviderUnreachable) { + fallbackTriggered = true; + } + } + + const actor = actorCalled(actorName); + await actor.attemptsTo(notes().set('signinRedirectInvoked', signinRedirectInvoked), notes().set('fallbackTriggered', fallbackTriggered)); + + // Clean up the temporary context + await page.close().catch(() => undefined); + await context.close().catch(() => undefined); +}); + +Then('{word} is taken to the sign-in flow', async (actorName: string) => { + const actor = actorCalled(actorName); + const invoked = await actor.answer(notes().get('signinRedirectInvoked')); + if (!invoked) { + throw new Error(`Expected ${actorName} to be taken to the sign-in flow, but the sign-in handler was not invoked`); + } +}); + +Then('{word} can still reach the sign-in page', async (actorName: string) => { + const actor = actorCalled(actorName); + const fallback = await actor.answer(notes().get('fallbackTriggered')); + if (!fallback) { + throw new Error(`Expected ${actorName} to reach the sign-in page via the fallback path, but the fallback was not triggered`); + } +}); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/index.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/index.ts new file mode 100644 index 000000000..c137e8181 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/index.ts @@ -0,0 +1,2 @@ +// Authentication context step definitions +import './header-login.steps.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts index a5f0e385e..880273cce 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts @@ -2,5 +2,6 @@ export { MongoDBTestServer } from '@ocom-verification/verification-shared/server export { PortlessServer } from './portless-server.ts'; export { TestApiServer } from './test-api-server.ts'; export { TestCommunityViteServer } from './test-community-vite-server.ts'; -export { buildUrl, cleanupTestEnvironment, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, setMongoConnectionString } from './test-environment.ts'; +export { buildUrl, cleanupTestEnvironment, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, mockStaffOidcIssuer, setMongoConnectionString } from './test-environment.ts'; export { TestOAuth2Server } from './test-oauth2-server.ts'; +export { TestStaffViteServer } from './test-staff-vite-server.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 906d75289..771c04c53 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -33,6 +33,8 @@ export const mockOidcIssuer = buildUrl('mock-auth.ownercommunity.localhost', '/c export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; export const mockOidcAudience = 'mock-client'; +export const mockStaffOidcIssuer = buildUrl('mock-auth.ownercommunity.localhost', '/staff'); + export function setMongoConnectionString(connStr: string): void { mongoConnectionString = connStr; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-staff-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-staff-vite-server.ts new file mode 100644 index 000000000..28eb60f83 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-staff-vite-server.ts @@ -0,0 +1,49 @@ +import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { PortlessServer } from './portless-server.ts'; +import { buildUrl, mockStaffOidcIssuer } from './test-environment.ts'; + +/** + * Starts the staff portal Vite dev server as a subprocess via `pnpm run dev`. + */ +export class TestStaffViteServer extends PortlessServer { + protected get probeUrl() { + return buildUrl('staff.ownercommunity.localhost'); + } + protected get readyMarker() { + return 'ready in'; + } + protected get serverName() { + return 'TestStaffViteServer'; + } + + protected override get startupTimeoutMs() { + return 60_000; + } + + protected get spawnArgs() { + return ['run', 'dev']; + } + protected get cwd() { + return apiSettings.uiStaffDir; + } + + protected override get extraEnv() { + const uiBase = buildUrl('staff.ownercommunity.localhost'); + const apiEndpoint = buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); + + return { + BROWSER: 'none', + NODE_ENV: 'development', + VITE_BASE_URL: uiBase, + VITE_APP_UI_STAFF_AAD_AUTHORITY: mockStaffOidcIssuer, + VITE_APP_UI_STAFF_AAD_REDIRECT_URI: `${uiBase}/auth-redirect`, + VITE_APP_UI_STAFF_AAD_CLIENTID: 'mock-client', + VITE_COMMON_API_ENDPOINT: apiEndpoint, + VITE_FUNCTION_ENDPOINT: apiEndpoint, + }; + } + + getUrl(): string { + return buildUrl('staff.ownercommunity.localhost'); + } +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index b1fa6a5bb..91c7c7e9e 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,12 +1,13 @@ import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server, TestStaffViteServer } from './servers/index.ts'; let mongoDBServer: MongoDBTestServer | undefined; let oauth2Server: TestOAuth2Server | undefined; let apiServer: TestApiServer | undefined; let communityViteServer: TestCommunityViteServer | undefined; +let staffViteServer: TestStaffViteServer | undefined; let apiUrl: string | undefined; let browser: Browser | undefined; let browserBaseUrl: string | undefined; @@ -17,10 +18,13 @@ let shutdownHandlersRegistered = false; export interface InfrastructureState { apiUrl: string | undefined; browseTheWeb: BrowseTheWeb | undefined; + staffBaseUrl: string | undefined; + communityBaseUrl: string | undefined; + browser: Browser | undefined; } export function getState(): InfrastructureState { - return { apiUrl, browseTheWeb }; + return { apiUrl, browseTheWeb, staffBaseUrl: staffViteServer?.getUrl(), communityBaseUrl: browserBaseUrl, browser }; } export async function stopAll(): Promise { @@ -39,6 +43,10 @@ export async function stopAll(): Promise { await communityViteServer.stop().catch(() => undefined); communityViteServer = undefined; } + if (staffViteServer) { + await staffViteServer.stop().catch(() => undefined); + staffViteServer = undefined; + } if (apiServer) { await apiServer.stop().catch(() => undefined); apiServer = undefined; @@ -78,8 +86,10 @@ export async function ensureE2EServers(): Promise { // Phase 2: Start API (needs MongoDB conn string), Vite (independent), and generate token (needs OAuth2) in parallel apiServer ??= new TestApiServer(); communityViteServer ??= new TestCommunityViteServer(); + staffViteServer ??= new TestStaffViteServer(); const api = apiServer; const vite = communityViteServer; + const staffVite = staffViteServer; const phase2: Promise[] = []; if (!api.isRunning()) { phase2.push( @@ -91,6 +101,9 @@ export async function ensureE2EServers(): Promise { if (!vite.isRunning()) { phase2.push(vite.start()); } + if (!staffVite.isRunning()) { + phase2.push(staffVite.start()); + } if (phase2.length > 0) await Promise.all(phase2); browserBaseUrl = communityViteServer.getUrl(); diff --git a/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts b/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts index 8349e7969..fb2ff8a4f 100644 --- a/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts +++ b/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts @@ -5,3 +5,4 @@ import '../shared/support/hooks.ts'; import '../contexts/community/step-definitions/index.ts'; +import '../contexts/authentication/step-definitions/index.ts'; diff --git a/packages/ocom-verification/verification-shared/src/pages/home.page.ts b/packages/ocom-verification/verification-shared/src/pages/home.page.ts new file mode 100644 index 000000000..435596ca3 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/pages/home.page.ts @@ -0,0 +1,18 @@ +import type { ElementHandle, PageAdapter } from './page-adapter.ts'; + +/** + * Home page object — represents the landing screen that contains the + * site header with sign-in controls. Works with both jsdom (acceptance + * UI tests) and Playwright (e2e tests) via the PageAdapter abstraction. + */ +export class HomePage { + constructor(private readonly adapter: PageAdapter) {} + + get signInButton(): ElementHandle { + return this.adapter.getByRole('button', { name: /Log In|Sign In/i }); + } + + async clickSignIn(): Promise { + await this.signInButton.click(); + } +} diff --git a/packages/ocom-verification/verification-shared/src/pages/index.ts b/packages/ocom-verification/verification-shared/src/pages/index.ts index 74494ab66..8c8ff6683 100644 --- a/packages/ocom-verification/verification-shared/src/pages/index.ts +++ b/packages/ocom-verification/verification-shared/src/pages/index.ts @@ -1,4 +1,5 @@ export { CommunityPage } from './community.page.ts'; +export { HomePage } from './home.page.ts'; export { LoginPage } from './login.page.ts'; export type { ElementHandle, @@ -9,7 +10,9 @@ export type { } from './page-adapter.ts'; export type { E2ECommunityPage, + E2EHomePage, E2ELoginPage, UiCommunityPage, + UiHomePage, UiLoginPage, } from './page-interfaces/index.ts'; diff --git a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/home.page-interface.ts b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/home.page-interface.ts new file mode 100644 index 000000000..e279f8c47 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/home.page-interface.ts @@ -0,0 +1,5 @@ +import type { HomePage } from '../home.page.ts'; + +export type UiHomePage = Pick; + +export type E2EHomePage = Pick; diff --git a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/index.ts b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/index.ts index fb0c2a804..9a095a538 100644 --- a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/index.ts +++ b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/index.ts @@ -2,6 +2,10 @@ export type { E2ECommunityPage, UiCommunityPage, } from './community.page-interface.ts'; +export type { + E2EHomePage, + UiHomePage, +} from './home.page-interface.ts'; export type { E2ELoginPage, UiLoginPage, diff --git a/packages/ocom-verification/verification-shared/src/scenarios/authentication/header-login.feature b/packages/ocom-verification/verification-shared/src/scenarios/authentication/header-login.feature new file mode 100644 index 000000000..a0514e4af --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/scenarios/authentication/header-login.feature @@ -0,0 +1,27 @@ +Feature: Sign In From Header + + As an unauthenticated visitor + I want to sign in from the site header + So that I can access my account + + Scenario: Visitor signs in to the community site + Given Alex visits the community site + When Alex chooses to sign in + Then Alex is taken to the sign-in flow + + Scenario: Visitor signs in to the staff site + Given Alex visits the staff site + When Alex chooses to sign in + Then Alex is taken to the sign-in flow + + Scenario: Community visitor can still reach sign-in when the identity provider is unreachable + Given Alex visits the community site + And the identity provider is unreachable + When Alex chooses to sign in + Then Alex can still reach the sign-in page + + Scenario: Staff visitor can still reach sign-in when the identity provider is unreachable + Given Alex visits the staff site + And the identity provider is unreachable + When Alex chooses to sign in + Then Alex can still reach the sign-in page diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index c28d7a6f5..d075b6b25 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -42,6 +42,7 @@ export const apiSettings = { apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), uiCommunityDir: path.dirname(uiEnvPath), + uiStaffDir: path.join(workspaceRoot, 'apps', 'ui-staff'), } as const; export const uiSettings = { diff --git a/packages/ocom/ui-community-route-root/src/components/header.tsx b/packages/ocom/ui-community-route-root/src/components/header.tsx index 65ccad217..9a2aaa931 100644 --- a/packages/ocom/ui-community-route-root/src/components/header.tsx +++ b/packages/ocom/ui-community-route-root/src/components/header.tsx @@ -11,12 +11,12 @@ export const Header: React.FC = () => { await auth.signinRedirect(); return; } - } catch (_err) { - // swallow and fall back below + } catch (err) { + console.error('OIDC signinRedirect failed, falling back to direct navigation', err); } // fall back to direct navigation if the OIDC helper is unavailable or fails - globalThis.location.href = `${import.meta.env.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI}`; + globalThis.location.href = `${(import.meta as { env?: ImportMetaEnv }).env?.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI ?? ''}`; }; const { diff --git a/packages/ocom/ui-staff-route-root/src/components/header.tsx b/packages/ocom/ui-staff-route-root/src/components/header.tsx index ad1cf626e..024e33481 100644 --- a/packages/ocom/ui-staff-route-root/src/components/header.tsx +++ b/packages/ocom/ui-staff-route-root/src/components/header.tsx @@ -13,12 +13,12 @@ export const Header: React.FC = () => { await auth.signinRedirect(); return; } - } catch (_err) { - // swallow and fall back below + } catch (err) { + console.error('OIDC signinRedirect failed, falling back to direct navigation', err); } // fall back to direct navigation if the OIDC helper is unavailable or fails - globalThis.location.href = `${import.meta.env.VITE_APP_UI_STAFF_AAD_REDIRECT_URI}`; + globalThis.location.href = `${(import.meta as { env?: ImportMetaEnv }).env?.VITE_APP_UI_STAFF_AAD_REDIRECT_URI ?? ''}`; }; const { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80d554b45..5ac55583c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -960,25 +960,25 @@ importers: version: link:../config-vitest '@chromatic-com/storybook': specifier: ^4.1.1 - version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-a11y': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-docs': specifier: ^9.1.3 - version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-onboarding': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) '@storybook/react': specifier: ^9.1.9 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) '@storybook/react-vite': specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -990,10 +990,10 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitest/browser': specifier: ^4.1.2 - version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/browser-playwright': specifier: ^4.1.2 - version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) @@ -1011,13 +1011,13 @@ importers: version: 6.0.1 storybook: specifier: 'catalog:' - version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: @@ -1118,6 +1118,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.2.0(react@19.2.0) + react-oidc-context: + specifier: ^3.3.0 + version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) std-env: specifier: ^4.0.0 version: 4.0.0 @@ -14917,6 +14920,18 @@ snapshots: '@blazediff/core@1.9.1': {} + '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@neoconfetti/react': 1.0.0 + chromatic: 13.3.4 + filesize: 10.1.6 + jsonfile: 6.2.0 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + strip-ansi: 7.1.2 + transitivePeerDependencies: + - '@chromatic-com/cypress' + - '@chromatic-com/playwright' + '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@neoconfetti/react': 1.0.0 @@ -16952,6 +16967,15 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + glob: 10.5.0 + magic-string: 0.30.21 + react-docgen-typescript: 2.4.0(typescript@6.0.3) + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + optionalDependencies: + typescript: 6.0.3 + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 10.5.0 @@ -18380,12 +18404,31 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@storybook/global': 5.0.0 + axe-core: 4.11.0 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.0 storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) + '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) @@ -18399,6 +18442,10 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18419,6 +18466,22 @@ snapshots: - react - react-dom + '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + prompts: 2.4.2 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + optionalDependencies: + '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/runner': 4.1.2 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - react + - react-dom + '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': dependencies: '@storybook/global': 5.0.0 @@ -18435,6 +18498,13 @@ snapshots: - react - react-dom + '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) @@ -18442,6 +18512,11 @@ snapshots: ts-dedent: 2.2.0 vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + unplugin: 1.16.1 + '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18454,12 +18529,38 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@rollup/pluginutils': 5.3.0 + '@storybook/builder-vite': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) + find-up: 7.0.0 + magic-string: 0.30.21 + react: 19.2.0 + react-docgen: 8.0.2 + react-dom: 19.2.0(react@19.2.0) + resolve: 1.22.11 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + tsconfig-paths: 4.2.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18480,6 +18581,16 @@ snapshots: - supports-color - typescript + '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + optionalDependencies: + typescript: 6.0.3 + '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 @@ -18983,6 +19094,19 @@ snapshots: - vite optional: true + '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + dependencies: + '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + playwright: 1.59.0 + tinyrainbow: 3.1.0 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) @@ -19014,6 +19138,23 @@ snapshots: - vite optional: true + '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@blazediff/core': 1.9.1 @@ -19064,6 +19205,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 @@ -19080,6 +19229,14 @@ snapshots: optionalDependencies: vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 @@ -25620,6 +25777,28 @@ snapshots: graphql: 16.12.0 react: 19.2.0 + storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@storybook/global': 5.0.0 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/spy': 3.2.4 + better-opn: 3.0.2 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + recast: 0.23.11 + semver: 7.7.4 + ws: 8.20.1 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - msw + - supports-color + - utf-8-validate + - vite + storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@storybook/global': 5.0.0 @@ -26415,6 +26594,26 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + esbuild: 0.25.12 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -26465,6 +26664,36 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.10.1 + '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + jsdom: 26.1.0 + transitivePeerDependencies: + - msw + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 From 71f50c632b0c4f4af41774c24d65c197c3b77df9 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 21 May 2026 15:35:56 -0400 Subject: [PATCH 38/38] small adjustments to tests for better verification --- .../step-definitions/header-login.steps.tsx | 8 +- .../step-definitions/header-login.steps.ts | 110 +++++++++++------- .../shared/support/shared-infrastructure.ts | 11 ++ .../ocom-verification/e2e-tests/src/world.ts | 4 +- 4 files changed, 88 insertions(+), 45 deletions(-) diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx index bb43163f1..4cffe5960 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx @@ -2,8 +2,8 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actorCalled, notes } from '@serenity-js/core'; import React from 'react'; import { AuthContext, type AuthContextProps } from 'react-oidc-context'; -import { Header as CommunityHeader } from '../../../../../../ocom/ui-community-route-root/src/components/header.tsx'; -import { Header as StaffHeader } from '../../../../../../ocom/ui-staff-route-root/src/components/header.tsx'; +import { SectionLayout as CommunitySectionLayout } from '../../../../../../ocom/ui-community-route-root/src/section-layout.tsx'; +import { SectionLayout as StaffSectionLayout } from '../../../../../../ocom/ui-staff-route-root/src/section-layout.tsx'; import { mountComponent } from '../../../shared/support/ui/react-render.ts'; import type { CellixUiWorld } from '../../../world.ts'; import type { HeaderUiNotes } from '../abilities/header-types.ts'; @@ -69,8 +69,8 @@ When('{word} chooses to sign in', async function (this: CellixUiWorld, _actorNam }; const authValue = { signinRedirect } as unknown as AuthContextProps; - const HeaderComponent = state.site === 'community' ? CommunityHeader : StaffHeader; - const wrapped = React.createElement(AuthContext.Provider, { value: authValue }, React.createElement(HeaderComponent)); + const PageComponent = state.site === 'community' ? CommunitySectionLayout : StaffSectionLayout; + const wrapped = React.createElement(AuthContext.Provider, { value: authValue }, React.createElement(PageComponent)); state.originalConsoleError = console.error; console.error = (..._args: unknown[]) => { diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts index 02f770172..1fbf8ad68 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -6,6 +6,7 @@ import * as infra from '../../../shared/support/shared-infrastructure.ts'; interface HeaderE2ENotes { signinRedirectInvoked: boolean; fallbackTriggered: boolean; + postLoginUrl: string; } type Site = 'community' | 'staff'; @@ -25,22 +26,40 @@ function getHeaderState(): HeaderE2EState { return state; } +/** Dispose the scenario's isolated browser context */ +async function cleanupHeaderContext(): Promise { + if (state?.page) { + await state.page.close().catch(() => undefined); + delete state.page; + } + if (state?.context) { + await state.context.close().catch(() => undefined); + delete state.context; + } +} + Given('{word} visits the community site', async (actorName: string) => { state = { actorName, site: 'community', identityProviderUnreachable: false }; const actor = actorCalled(actorName); - await actor.attemptsTo(notes().set('signinRedirectInvoked', false), notes().set('fallbackTriggered', false)); + await actor.attemptsTo(notes().set('signinRedirectInvoked', false), notes().set('fallbackTriggered', false), notes().set('postLoginUrl', '')); }); Given('{word} visits the staff site', async (actorName: string) => { state = { actorName, site: 'staff', identityProviderUnreachable: false }; const actor = actorCalled(actorName); - await actor.attemptsTo(notes().set('signinRedirectInvoked', false), notes().set('fallbackTriggered', false)); + await actor.attemptsTo(notes().set('signinRedirectInvoked', false), notes().set('fallbackTriggered', false), notes().set('postLoginUrl', '')); }); Given('the identity provider is unreachable', () => { getHeaderState().identityProviderUnreachable = true; }); +// Credentials from apps/ui-{portal}/mock-oidc.users.json +const portalCredentials: Record = { + community: { username: 'test@example.com', password: 'password' }, + staff: { username: 'staff@ownercommunity.onmicrosoft.com', password: 'password' }, +}; + When('{word} chooses to sign in', async (actorName: string) => { const s = getHeaderState(); s.actorName = actorName; @@ -50,14 +69,14 @@ When('{word} chooses to sign in', async (actorName: string) => { const baseUrl = s.site === 'community' ? (infra.getState().communityBaseUrl ?? 'https://ownercommunity.localhost:1355') : (infra.getState().staffBaseUrl ?? 'https://staff.ownercommunity.localhost:1355'); - // Create a fresh unauthenticated context for this scenario + // Fresh unauthenticated context — isolated from the pre-auth context + // used by other test suites. Cleaned up in the Then step after verification. const context = await browser.newContext({ baseURL: baseUrl, ignoreHTTPSErrors: true, }); s.context = context; - // If identity provider is unreachable, block all requests to mock-auth if (s.identityProviderUnreachable) { await context.route('**/mock-auth.**', (route) => route.abort('connectionrefused')); } @@ -65,55 +84,66 @@ When('{word} chooses to sign in', async (actorName: string) => { const page = await context.newPage(); s.page = page; - // Navigate to the site root (unauthenticated header is visible) + // Navigate to site root — the unauthenticated home page is visible await page.goto('/', { waitUntil: 'networkidle', timeout: 30_000 }); - // Click the sign-in button via the shared page object pattern + // Click the sign-in button on the home page const signInButton = page.getByRole('button', { name: /Log In|Sign In/i }); await signInButton.click(); - // Determine outcome based on whether the IdP redirect happened - let signinRedirectInvoked = false; - let fallbackTriggered = false; - - try { - if (s.identityProviderUnreachable) { - // With IdP blocked, the app should handle the error gracefully. - // Wait briefly for error handling to settle. - await page.waitForTimeout(2000); - fallbackTriggered = true; - } else { - // Should redirect to mock-auth or complete auth flow - await page.waitForURL((url) => url.hostname.includes('mock-auth') || url.pathname.includes('auth-redirect'), { timeout: 15_000 }); - signinRedirectInvoked = true; - } - } catch { - // If waiting for URL timed out with IdP unreachable, fallback is triggered - if (s.identityProviderUnreachable) { - fallbackTriggered = true; + if (s.identityProviderUnreachable) { + // IdP is blocked — the app should handle the error gracefully. + // Wait for error handling to settle, then leave the page open for Then to inspect. + await page.waitForTimeout(2000); + } else { + // Wait for redirect to mock-auth login form + await page.waitForURL((url) => url.hostname.includes('mock-auth'), { timeout: 15_000 }); + + // Complete the login form with portal-specific credentials + const creds = portalCredentials[s.site]; + if (page.url().includes('/login')) { + await page.fill('input[name="username"]', creds.username); + await page.fill('input[name="password"]', creds.password); + await page.click('button[type="submit"]'); } - } - - const actor = actorCalled(actorName); - await actor.attemptsTo(notes().set('signinRedirectInvoked', signinRedirectInvoked), notes().set('fallbackTriggered', fallbackTriggered)); - // Clean up the temporary context - await page.close().catch(() => undefined); - await context.close().catch(() => undefined); + // Wait for the redirect chain to settle back on the portal + await page.waitForURL((url) => !url.hostname.includes('mock-auth') && !url.pathname.includes('auth-redirect'), { timeout: 30_000 }); + await page.waitForLoadState('networkidle'); + } }); Then('{word} is taken to the sign-in flow', async (actorName: string) => { - const actor = actorCalled(actorName); - const invoked = await actor.answer(notes().get('signinRedirectInvoked')); - if (!invoked) { - throw new Error(`Expected ${actorName} to be taken to the sign-in flow, but the sign-in handler was not invoked`); + const { page } = getHeaderState(); + if (!page) throw new Error('No page — did the When step run?'); + + try { + // Verify the page actually landed back on the portal (not stuck on mock-auth) + const currentUrl = new URL(page.url()); + if (currentUrl.hostname.includes('mock-auth')) { + throw new Error(`Expected ${actorName} to complete the sign-in flow, but the page is still on the IdP: ${page.url()}`); + } + if (currentUrl.pathname.includes('auth-redirect')) { + throw new Error(`Expected ${actorName} to complete the sign-in flow, but the page is stuck on the auth redirect callback: ${page.url()}`); + } + } finally { + await cleanupHeaderContext(); } }); Then('{word} can still reach the sign-in page', async (actorName: string) => { - const actor = actorCalled(actorName); - const fallback = await actor.answer(notes().get('fallbackTriggered')); - if (!fallback) { - throw new Error(`Expected ${actorName} to reach the sign-in page via the fallback path, but the fallback was not triggered`); + const { page } = getHeaderState(); + if (!page) throw new Error('No page — did the When step run?'); + + try { + // With the IdP unreachable, the header's fallback should have fired + // (direct navigation to the redirect URI). The page should NOT be on + // mock-auth (which was blocked). + const currentUrl = new URL(page.url()); + if (currentUrl.hostname.includes('mock-auth')) { + throw new Error(`Expected ${actorName} to reach the sign-in page via fallback, but somehow ended up on mock-auth: ${page.url()}`); + } + } finally { + await cleanupHeaderContext(); } }); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index 91c7c7e9e..cb1a4e12e 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -27,6 +27,17 @@ export function getState(): InfrastructureState { return { apiUrl, browseTheWeb, staffBaseUrl: staffViteServer?.getUrl(), communityBaseUrl: browserBaseUrl, browser }; } +/** + * Resets mutable state between scenarios without restarting servers. + * Drops all MongoDB collections and re-seeds reference data so each + * scenario starts from a clean baseline. + */ +export async function resetScenarioState(): Promise { + if (mongoDBServer?.isRunning()) { + await mongoDBServer.resetForScenario(); + } +} + export async function stopAll(): Promise { if (browseTheWeb) { await browseTheWeb.close().catch(() => undefined); diff --git a/packages/ocom-verification/e2e-tests/src/world.ts b/packages/ocom-verification/e2e-tests/src/world.ts index 2730b9dd1..e40e2eb21 100644 --- a/packages/ocom-verification/e2e-tests/src/world.ts +++ b/packages/ocom-verification/e2e-tests/src/world.ts @@ -21,7 +21,9 @@ export class CellixE2EWorld extends World { } async cleanup(): Promise { - // Reuse same browser session across scenarios. + // Reset DB state between scenarios so each starts from a clean baseline. + // Servers stay running — only mutable data is cleared and re-seeded. + await infra.resetScenarioState(); } }