Covers 5 scenarios with extensive inline comments explaining business context and the 2026-03-25 stale-cache root-cause fix: 1. Auth gate (Sev-1 regression guard) — no UUID → Access Denied 2. Happy path — valid UUID + fresh cfg → access granted 3. Invalid UUID — Novi 404 → Access Denied 4. Stale cache — StorageEvent delivers fresh site_cfg_json → Effect 2 retries verification without reload (tests the reactive tracking fix in (idaa)/+layout.svelte) 5. iframe mode — Reload/Retry button visible on Access Denied Key lesson found while writing: ae_idaa_loc seed must include the full bb object or verify_novi_uuid() throws on bb.qry__hidden assignment, caught silently, resetting novi_uuid to null even after a successful Novi API call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
527 lines
25 KiB
TypeScript
527 lines
25 KiB
TypeScript
/*
|
|
* Playwright tests: IDAA Novi UUID Authentication
|
|
*
|
|
* WHAT THIS FILE TESTS
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* The IDAA module is embedded inside Novi member pages as an iframe. When Novi
|
|
* renders the host page it injects the current member's UUID into the iframe src:
|
|
*
|
|
* src="/idaa/archives?uuid=<CustomerUniqueId>&iframe=true&key=..."
|
|
*
|
|
* The IDAA sub-layout (src/routes/idaa/(idaa)/+layout.svelte) reads that UUID,
|
|
* calls the external Novi API to verify it is a real member, and then grants the
|
|
* appropriate access level (anonymous → authenticated → trusted → administrator).
|
|
*
|
|
* WHY THESE TESTS EXIST
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* 1. PRIVACY — A prior AI agent accidentally made IDAA content public. The auth
|
|
* gate test (test 1) is the regression guard. It must always run first.
|
|
*
|
|
* 2. ROOT-CAUSE FIX COVERAGE — The Dexie cache fast-path can return a stale
|
|
* site_domain record whose cfg_json is missing `novi_idaa_api_key`. When that
|
|
* happens verification fails silently and the user sees "Access Denied". Two
|
|
* files were changed to fix this:
|
|
*
|
|
* ae_core__site.ts — after background refresh saves to Dexie, push
|
|
* fresh cfg_json into $ae_loc so store subscribers
|
|
* are notified.
|
|
* idaa/(idaa)/+layout.svelte — track $ae_loc.site_cfg_json outside untrack()
|
|
* so Effect 2 re-runs when cfg arrives; guard
|
|
* prevents double-verification if already done.
|
|
*
|
|
* Test 4 specifically exercises this stale-cache → auto-retry path.
|
|
*
|
|
* MOCKING STRATEGY
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* • All /v3/ API calls are intercepted with a page.route glob (any-origin /v3/ any-path).
|
|
* Only site_domain/search needs a specific response; everything else returns [].
|
|
*
|
|
* • The external Novi API (https://www.idaa.org/api/customers/:uuid) is
|
|
* intercepted with a page.route glob (any-origin /api/customers/ any-path).
|
|
* This prevents any real network traffic to idaa.org during CI/dev runs.
|
|
*
|
|
* • localStorage is seeded with page.addInitScript() before each navigate so the
|
|
* persisted $ae_loc and $ae_idaa_loc stores start in a known state.
|
|
*
|
|
* ROUTE UNDER TEST
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* /idaa/archives — simplest IDAA sub-route; auth gate fires at the shared
|
|
* (idaa)/+layout.svelte before any child content loads, so
|
|
* no archive fixture data is needed for the auth tests.
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import { ae_app_local_data_defaults } from './_helpers/ae_defaults';
|
|
import { testing_account_id, mock_site_domain } from './_helpers/env';
|
|
|
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Synthetic Novi UUID used as the test subject.
|
|
* Must NOT be a real member UUID — nothing here reaches the real Novi API.
|
|
* This UUID is also listed in the fresh_site_cfg_json novi_trusted_li so that
|
|
* a verified member gets 'trusted' access (not just 'authenticated').
|
|
*/
|
|
const TEST_NOVI_UUID = 'c9ea07b5-06b0-4a43-a2d0-8d06558c8a82';
|
|
|
|
/**
|
|
* Synthetic Novi API key stored in site_cfg_json.novi_idaa_api_key.
|
|
* The layout uses this as a Basic auth header when calling the Novi API.
|
|
* Any non-empty string works here — the mocked route never inspects it.
|
|
*/
|
|
const TEST_NOVI_API_KEY = 'Basic dGVzdC10ZXN0LXRlc3Q=';
|
|
|
|
/**
|
|
* Minimal Novi API member payload.
|
|
* Matches the shape that verify_novi_uuid() reads in (idaa)/+layout.svelte:
|
|
* FirstName, LastName → display name ("IDAA T.")
|
|
* Email → stored as idaa_loc.novi_email
|
|
*/
|
|
const mock_novi_member = {
|
|
FirstName: 'IDAA',
|
|
LastName: 'TestMember',
|
|
Name: 'IDAA TestMember',
|
|
Email: 'test+novi@oneskyit.com',
|
|
CustomerUniqueId: TEST_NOVI_UUID
|
|
};
|
|
|
|
/** IDAA sub-route to navigate to. All auth tests use this path. */
|
|
const IDAA_ROUTE = '/idaa/archives';
|
|
|
|
// ─── site_cfg_json factories ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Fresh (correct) site_cfg_json — what the API returns after a Novi API key
|
|
* is properly configured. This is the happy-path config.
|
|
*
|
|
* novi_trusted_li includes TEST_NOVI_UUID so verified members get 'trusted'
|
|
* access, which satisfies the (idaa)/+layout.svelte gate condition.
|
|
*/
|
|
function fresh_site_cfg_json() {
|
|
return {
|
|
slct__event_id: null,
|
|
novi_idaa_api_key: TEST_NOVI_API_KEY,
|
|
novi_api_root_url: 'https://www.idaa.org/api',
|
|
novi_admin_li: [],
|
|
novi_trusted_li: [TEST_NOVI_UUID]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Stale (broken) site_cfg_json — what Dexie might have cached if the site
|
|
* record was updated server-side after the user's last visit. Specifically
|
|
* missing novi_idaa_api_key, which causes verify_novi_uuid() to bail early.
|
|
*
|
|
* This is the cfg that triggers the bug covered by test 4.
|
|
*/
|
|
function stale_site_cfg_json() {
|
|
return {
|
|
slct__event_id: null
|
|
// Intentionally omitted: novi_idaa_api_key, novi_api_root_url, novi_trusted_li
|
|
};
|
|
}
|
|
|
|
// ─── Route mock helpers ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Mock all /v3/ API traffic for IDAA auth tests.
|
|
*
|
|
* Only site_domain/search needs a real response — everything else (archive
|
|
* lists, event lists, etc.) can safely return an empty array because the auth
|
|
* gate fires before any child content renders.
|
|
*
|
|
* @param cfg_json The cfg_json to embed in the site_domain response.
|
|
* Use fresh_site_cfg_json() for normal operation.
|
|
* Use stale_site_cfg_json() to simulate a stale Dexie cache.
|
|
*/
|
|
async function mock_v3_routes(page: any, cfg_json: any) {
|
|
await page.route('**/v3/**', async (route: any) => {
|
|
const url = route.request().url();
|
|
|
|
// Bootstrap call: +layout.ts looks up the current domain to get account_id
|
|
// and site_cfg_json. This is where the stale-cache bug manifests.
|
|
if (url.includes('site_domain/search')) {
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
data: [{ ...mock_site_domain, cfg_json }]
|
|
})
|
|
});
|
|
}
|
|
|
|
// Everything else — archives, events, lookup tables — returns empty.
|
|
// The auth gate fires before child content renders so these are never used.
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ data: [] })
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mock the external Novi API endpoint called by verify_novi_uuid().
|
|
*
|
|
* URL pattern: https://www.idaa.org/api/customers/<uuid>
|
|
* Playwright intercepts cross-origin requests from the page context, so this
|
|
* stops any real traffic from leaving the test environment.
|
|
*
|
|
* @param status 200 for a recognised member, 404 for an unknown UUID, or
|
|
* any other status to simulate server errors.
|
|
* @param body Response body. For 200 pass mock_novi_member. 404 can be empty.
|
|
*/
|
|
async function mock_novi_api(page: any, status: number, body?: any) {
|
|
await page.route('**/api/customers/**', async (route: any) => {
|
|
return route.fulfill({
|
|
status,
|
|
contentType: 'application/json',
|
|
body: body ? JSON.stringify(body) : '{}'
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── localStorage seed helpers ────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Seed both ae_loc and ae_idaa_loc into localStorage before page load.
|
|
*
|
|
* Must be called via page.addInitScript() which runs before any page JS so that
|
|
* store_versions.ts sees the correct __version and leaves the stores intact.
|
|
*
|
|
* ae_loc is seeded as fully anonymous (no auth flags set) — the IDAA layout must
|
|
* earn access via Novi verification, not inherit it from a cached session.
|
|
*
|
|
* ae_idaa_loc is seeded as a clean slate (no cached UUID) so each test starts
|
|
* fresh and does not inherit a verified state from a previous test run.
|
|
*
|
|
* @param ae_loc_overrides Merged on top of the defaults. Use this to set
|
|
* iframe:true, or to inject a stale site_cfg_json for the stale-cache test.
|
|
*/
|
|
async function seed_anonymous_session(page: any, ae_loc_overrides: any = {}) {
|
|
await page.addInitScript(
|
|
({ defaults, account_id, overrides }: any) => {
|
|
// Fully anonymous ae_loc — no auth flags
|
|
const ae_loc_data = {
|
|
...defaults,
|
|
account_id,
|
|
allow_access: true,
|
|
access_type: 'anonymous',
|
|
authenticated_access: false,
|
|
trusted_access: false,
|
|
administrator_access: false,
|
|
...overrides
|
|
};
|
|
window.localStorage.setItem('ae_loc', JSON.stringify(ae_loc_data));
|
|
|
|
// Clean ae_idaa_loc — no cached UUID from previous sessions.
|
|
// IMPORTANT: must include all nested objects (bb, archives, recovery_meetings)
|
|
// that the IDAA layout writes to after verification. If bb is missing,
|
|
// verify_novi_uuid() throws "Cannot set properties of undefined" when it
|
|
// tries to reset bb.qry__hidden — caught by the try/catch which then nulls
|
|
// novi_uuid, making the UUID span invisible even when access is granted.
|
|
const ae_idaa_loc_data = {
|
|
ver: '2024-08-21_1646',
|
|
novi_uuid: null,
|
|
novi_email: null,
|
|
novi_full_name: null,
|
|
novi_verified: false,
|
|
novi_admin_li: [],
|
|
novi_trusted_li: [],
|
|
novi_jitsi_mod_li: [],
|
|
ds: {},
|
|
idaa_cfg_json: {},
|
|
// bb is required — layout writes to bb.qry__hidden and bb.qry__enabled
|
|
// at the end of a successful verification pass.
|
|
bb: {
|
|
enabled: 'enabled',
|
|
hidden: 'not_hidden',
|
|
limit: 50,
|
|
offset: 0,
|
|
edit_kv: {},
|
|
edit__post_obj: null,
|
|
edit__post_comment_obj: null,
|
|
show_list__post_obj_li: true,
|
|
qry__enabled: 'enabled',
|
|
qry__hidden: 'not_hidden',
|
|
qry__limit: 25,
|
|
qry__offset: 0,
|
|
qry__order_by: 'updated_on',
|
|
qry__order_by_li: { updated_on: 'DESC', created_on: 'DESC' }
|
|
},
|
|
archives: {
|
|
enabled: 'enabled',
|
|
hidden: 'not_hidden',
|
|
limit: 150,
|
|
offset: 0,
|
|
edit_kv: {},
|
|
edit__archive_obj: null,
|
|
edit__archive_content_obj: null
|
|
}
|
|
};
|
|
window.localStorage.setItem(
|
|
'ae_idaa_loc',
|
|
JSON.stringify(ae_idaa_loc_data)
|
|
);
|
|
|
|
// Suppress outbound email calls during tests (checked in api.ts)
|
|
(window as any).__ae_test_mode = true;
|
|
},
|
|
{
|
|
defaults: ae_app_local_data_defaults,
|
|
account_id: testing_account_id,
|
|
overrides: ae_loc_overrides
|
|
}
|
|
);
|
|
}
|
|
|
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
test.describe('IDAA Novi UUID authentication', () => {
|
|
// Pipe browser-side JS errors to test stdout so failures are diagnosable
|
|
// without opening Playwright's trace viewer.
|
|
test.beforeEach(async ({ page }) => {
|
|
page.on('pageerror', (err) =>
|
|
console.error(`BROWSER ERROR: ${err.message}`)
|
|
);
|
|
});
|
|
|
|
// ── Test 1: Auth gate ─────────────────────────────────────────────────────
|
|
//
|
|
// THIS TEST MUST RUN FIRST AND MUST ALWAYS PASS.
|
|
//
|
|
// IDAA = International Doctors in Alcoholics Anonymous. All content is
|
|
// strictly private. A prior AI agent accidentally made IDAA BB public —
|
|
// this test is the regression guard for the access gate.
|
|
//
|
|
// If this test fails it means unauthenticated visitors can see IDAA content.
|
|
// That is a Sev-1 privacy failure regardless of any other test results.
|
|
|
|
test('auth gate: no UUID and no auth → Access Denied is shown', async ({
|
|
page
|
|
}) => {
|
|
// Stale cfg is fine here — the point is that no UUID means no verification
|
|
await mock_v3_routes(page, stale_site_cfg_json());
|
|
await seed_anonymous_session(page);
|
|
|
|
// Navigate WITHOUT a uuid param — simulates a direct URL visit, a search
|
|
// engine crawler, or a Novi member who hasn't been given the iframe URL
|
|
await page.goto(IDAA_ROUTE);
|
|
|
|
// The auth gate in (idaa)/+layout.svelte must block this request.
|
|
// "Access Denied" heading is the canonical signal that the gate fired.
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Access Denied' })
|
|
).toBeVisible({ timeout: 5000 });
|
|
|
|
// Confirm no private content leaked through — "IDAA Novi UUID not found!"
|
|
// is shown inside the denied block when uuid verification was not attempted.
|
|
// This proves the gate rendered correctly, not just that something went wrong.
|
|
await expect(page.getByText('IDAA Novi UUID not found!')).toBeVisible();
|
|
});
|
|
|
|
// ── Test 2: Happy path ────────────────────────────────────────────────────
|
|
//
|
|
// Fresh site_cfg_json has novi_idaa_api_key → verify_novi_uuid() runs.
|
|
// Novi API returns a valid member → access is granted.
|
|
// This is the normal day-to-day flow for Novi members visiting IDAA pages.
|
|
|
|
test('happy path: valid UUID + fresh cfg → access granted', async ({
|
|
page
|
|
}) => {
|
|
// Fresh cfg has novi_idaa_api_key so the layout can call the Novi API
|
|
await mock_v3_routes(page, fresh_site_cfg_json());
|
|
|
|
// Novi confirms this UUID is a real member
|
|
await mock_novi_api(page, 200, mock_novi_member);
|
|
|
|
// Start anonymous — access must come entirely from Novi verification
|
|
await seed_anonymous_session(page);
|
|
|
|
// Navigate with UUID param — same URL shape as the Novi iframe template
|
|
await page.goto(`${IDAA_ROUTE}?uuid=${TEST_NOVI_UUID}`);
|
|
|
|
// "Access Denied" must not appear after successful verification.
|
|
// Timeout is generous to allow the async Novi API round-trip to complete.
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Access Denied' })
|
|
).not.toBeVisible({ timeout: 6000 });
|
|
|
|
// The layout renders $idaa_loc.novi_uuid in a footer span after auth.
|
|
// Its presence confirms the full verification pipeline ran and the store
|
|
// was written — not just that the gate condition happened to pass.
|
|
// UUID appears inside a longer span ("Novi: [icon] [uuid] [name] [email]"),
|
|
// so exact:false is required — getByText default is whole-text match.
|
|
await expect(page.getByText(TEST_NOVI_UUID, { exact: false })).toBeVisible({
|
|
timeout: 6000
|
|
});
|
|
});
|
|
|
|
// ── Test 3: Invalid UUID ──────────────────────────────────────────────────
|
|
//
|
|
// UUID is present in the URL but the Novi API does not recognise it
|
|
// (e.g. expired membership, wrong UUID, typo in the Novi template).
|
|
// The user must be denied access — IDAA is not publicly readable.
|
|
|
|
test('invalid UUID: Novi API 404 → Access Denied with UUID not found', async ({
|
|
page
|
|
}) => {
|
|
await mock_v3_routes(page, fresh_site_cfg_json());
|
|
|
|
// Novi API does not recognise this UUID
|
|
await mock_novi_api(page, 404);
|
|
|
|
await seed_anonymous_session(page);
|
|
await page.goto(`${IDAA_ROUTE}?uuid=${TEST_NOVI_UUID}`);
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Access Denied' })
|
|
).toBeVisible({ timeout: 5000 });
|
|
|
|
// The specific "UUID not found!" message confirms the failure is the
|
|
// post-verification denial path, not the no-uuid path from test 1.
|
|
await expect(page.getByText('IDAA Novi UUID not found!')).toBeVisible();
|
|
});
|
|
|
|
// ── Test 4: Stale cache → reactive retry ─────────────────────────────────
|
|
//
|
|
// THIS TEST COVERS THE SPECIFIC BUG FIXED ON 2026-03-25.
|
|
//
|
|
// Scenario: $ae_loc.site_cfg_json is stale (missing novi_idaa_api_key —
|
|
// e.g. key was added server-side after the user's last visit, and their
|
|
// persisted store still has the old cfg). Effect 2 in the IDAA layout
|
|
// reads site_cfg_json → key is null → verify_novi_uuid() bails → Access Denied.
|
|
//
|
|
// The fix: Effect 2 now tracks $ae_loc.site_cfg_json OUTSIDE untrack(), so
|
|
// when the store is updated later (by any code path — background refresh,
|
|
// admin panel, etc.) the effect re-runs automatically and retries verification.
|
|
//
|
|
// This test proves the reactive-tracking half of the fix by simulating the
|
|
// store update directly via a StorageEvent, which is what svelte-persisted-store
|
|
// uses internally when another code path writes to $ae_loc. The test:
|
|
//
|
|
// 1. Seeds localStorage with stale cfg (no api_key)
|
|
// 2. Navigates → Access Denied (verify fails immediately)
|
|
// 3. Dispatches a StorageEvent to push fresh cfg into $ae_loc
|
|
// 4. Effect 2 re-runs (reactive tracking), retries with real api_key
|
|
// 5. Novi API returns 200 → access granted — no reload needed
|
|
//
|
|
// WHY StorageEvent instead of pre-seeding Dexie:
|
|
// In the test environment, Dexie is empty on first load, so lookup_site_domain
|
|
// takes the slow path (single API call, no background refresh). Pre-seeding
|
|
// Dexie would require an extra navigation-and-reload cycle and tightly couples
|
|
// this test to the ae_core__site.ts background-refresh plumbing. The StorageEvent
|
|
// approach tests the reactive tracking fix in isolation — cleaner and faster.
|
|
|
|
test('stale cache: $ae_loc.site_cfg_json update triggers Effect 2 retry → access granted without reload', async ({
|
|
page
|
|
}) => {
|
|
// Site domain always returns stale cfg for this test — the update comes via
|
|
// StorageEvent below, not from a background refresh.
|
|
await mock_v3_routes(page, stale_site_cfg_json());
|
|
|
|
// Novi API is primed and ready — it only gets called AFTER Effect 2 re-runs
|
|
// with the fresh api_key (step 4 above). If verification never retries this
|
|
// mock is never invoked and the test fails at the access-granted assertion.
|
|
await mock_novi_api(page, 200, mock_novi_member);
|
|
|
|
// Seed localStorage with stale site_cfg_json.
|
|
// This replicates a real user's persisted store after a server-side config change.
|
|
await seed_anonymous_session(page, {
|
|
site_cfg_json: stale_site_cfg_json()
|
|
});
|
|
|
|
await page.goto(`${IDAA_ROUTE}?uuid=${TEST_NOVI_UUID}`);
|
|
|
|
// Phase 1 — stale cfg causes immediate verification failure.
|
|
// verify_novi_uuid() exits early (no api_key) and sets novi_verified=false.
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Access Denied' })
|
|
).toBeVisible({ timeout: 5000 });
|
|
|
|
// Phase 2 — simulate $ae_loc.site_cfg_json being updated externally.
|
|
//
|
|
// svelte-persisted-store subscribes to window 'storage' events so that
|
|
// writes from other tabs/contexts (e.g. ae_core__site.ts after a background
|
|
// refresh) propagate into the in-memory store. Dispatching the event
|
|
// manually here has the same effect: the store updates in-memory, Svelte
|
|
// marks Effect 2 dirty (site_cfg_json was a tracked dependency), and the
|
|
// effect re-runs on the next microtask.
|
|
await page.evaluate(
|
|
({ fresh_cfg }: { fresh_cfg: any }) => {
|
|
const raw = window.localStorage.getItem('ae_loc');
|
|
const current = raw ? JSON.parse(raw) : {};
|
|
const updated = { ...current, site_cfg_json: fresh_cfg };
|
|
const newValue = JSON.stringify(updated);
|
|
window.localStorage.setItem('ae_loc', newValue);
|
|
// Dispatch synthetic storage event — triggers svelte-persisted-store
|
|
// listener in the same tab (native 'storage' event only fires in
|
|
// OTHER tabs; manual dispatch reaches same-tab listeners).
|
|
window.dispatchEvent(
|
|
new StorageEvent('storage', {
|
|
key: 'ae_loc',
|
|
newValue,
|
|
storageArea: window.localStorage
|
|
})
|
|
);
|
|
},
|
|
{ fresh_cfg: fresh_site_cfg_json() }
|
|
);
|
|
|
|
// Phase 3 — Effect 2 re-runs with fresh cfg, calls Novi API, grants access.
|
|
// Access Denied must disappear WITHOUT a page reload.
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Access Denied' })
|
|
).not.toBeVisible({ timeout: 8000 });
|
|
|
|
// UUID in footer span confirms full verification pipeline completed and
|
|
// $idaa_loc.novi_uuid was written — not just that the gate check passed.
|
|
await expect(page.getByText(TEST_NOVI_UUID, { exact: false })).toBeVisible({
|
|
timeout: 8000
|
|
});
|
|
});
|
|
|
|
// ── Test 5: Reload button in iframe Access Denied ─────────────────────────
|
|
//
|
|
// The Reload / Retry button is a UX stopgap for the stale-cache race.
|
|
// Even after the root-cause fix, edge cases may still show Access Denied
|
|
// (e.g. Novi API is slow or temporarily down). The button lets the user
|
|
// self-recover without involving the Novi site operators.
|
|
//
|
|
// The button is intentionally ONLY shown in iframe mode — outside an iframe
|
|
// the regular browser reload button is accessible and the extra UI is noise.
|
|
|
|
test('iframe mode: Reload/Retry button is visible on Access Denied', async ({
|
|
page
|
|
}) => {
|
|
// Force Access Denied by: stale cfg (no api_key) + Novi 404 (invalid UUID)
|
|
// This guarantees we stay on the denied screen for the assertion.
|
|
await mock_v3_routes(page, stale_site_cfg_json());
|
|
await mock_novi_api(page, 404);
|
|
|
|
// Seed with iframe:true — same as what the root layout sets when
|
|
// ?iframe=true is in the URL. We seed it directly to avoid relying
|
|
// on the URL param being processed before the layout mounts.
|
|
await seed_anonymous_session(page, { iframe: true });
|
|
|
|
// Navigate with iframe=true and a UUID — same URL as the Novi embed
|
|
await page.goto(
|
|
`${IDAA_ROUTE}?uuid=${TEST_NOVI_UUID}&iframe=true`
|
|
);
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Access Denied' })
|
|
).toBeVisible({ timeout: 5000 });
|
|
|
|
// Reload / Retry button must be present (added in fix commit 2026-03-25)
|
|
await expect(
|
|
page.getByRole('button', { name: /Reload \/ Retry/i })
|
|
).toBeVisible();
|
|
|
|
// Sanity-check: button is scoped to iframe context — the #ae_idaa div
|
|
// gets class="iframe" when $ae_loc.iframe is true (idaa/+layout.svelte).
|
|
// If this fails the iframe flag was not respected by the layout.
|
|
await expect(page.locator('#ae_idaa.iframe')).toBeVisible();
|
|
});
|
|
});
|