|
| 1 | +import { expect, test } from '@playwright/test'; |
| 2 | + |
| 3 | +/** |
| 4 | + * The lab renders four real child injectors, each with its own service, cipher |
| 5 | + * and forged font. It sits behind `@defer (on viewport)` because a child |
| 6 | + * service instantiated during prerender would write to the one shared |
| 7 | + * TransferState seed key and clobber the shell's cipher. |
| 8 | + */ |
| 9 | +test.describe('the configuration lab', () => { |
| 10 | + test.beforeEach(async ({ page }) => { |
| 11 | + await page.goto('./'); |
| 12 | + await page.locator('app-config-lab').scrollIntoViewIfNeeded(); |
| 13 | + await expect(page.locator('.config-sample').first()).toBeVisible(); |
| 14 | + }); |
| 15 | + |
| 16 | + test('renders four independent instances', async ({ page }) => { |
| 17 | + await expect(page.locator('.config-sample')).toHaveCount(4); |
| 18 | + }); |
| 19 | + |
| 20 | + test('no two forged fonts share a family name', async ({ page }) => { |
| 21 | + // Regression: familyName derives from the seed alone, so instances without |
| 22 | + // an explicit seed inherit the page's and register conflicting cmaps under |
| 23 | + // one family. The browser then paints with whichever face it picked. |
| 24 | + const forged = await page.evaluate(() => |
| 25 | + [...document.fonts].map((f) => f.family).filter((f) => /^(NoAi|Lab)-/.test(f)), |
| 26 | + ); |
| 27 | + |
| 28 | + expect(forged.length).toBeGreaterThan(1); |
| 29 | + expect(new Set(forged).size, `duplicate forged families: ${forged.join(', ')}`).toBe( |
| 30 | + forged.length, |
| 31 | + ); |
| 32 | + }); |
| 33 | + |
| 34 | + test('the pinned-seed instance uses the seed it was given', async ({ page }) => { |
| 35 | + const card = page.locator('app-config-lab .card').filter({ hasText: 'Pinned cipher' }); |
| 36 | + const stack = await card |
| 37 | + .locator('.config-sample') |
| 38 | + .evaluate((el) => getComputedStyle(el).fontFamily); |
| 39 | + |
| 40 | + expect(stack).toContain(`NoAi-${(12345).toString(36)}`); |
| 41 | + }); |
| 42 | + |
| 43 | + test('the kill switch leaves its text readable and forges nothing', async ({ page }) => { |
| 44 | + const card = page.locator('app-config-lab .card').filter({ hasText: 'Kill switch' }); |
| 45 | + const sample = card.locator('.config-sample'); |
| 46 | + |
| 47 | + await expect(sample).toContainText('Untouched text'); |
| 48 | + expect(await sample.evaluate((el) => getComputedStyle(el).fontFamily)).not.toMatch(/NoAi-/); |
| 49 | + }); |
| 50 | + |
| 51 | + test('the kill switch does not hide readable text from assistive technology', async ({ |
| 52 | + page, |
| 53 | + }) => { |
| 54 | + // aria-hidden belongs on ciphered specimens only. Applying it to text that |
| 55 | + // is genuinely readable imposes the cost without the protection. |
| 56 | + const sample = page |
| 57 | + .locator('app-config-lab .card') |
| 58 | + .filter({ hasText: 'Kill switch' }) |
| 59 | + .locator('.config-sample'); |
| 60 | + |
| 61 | + await expect(sample).not.toHaveAttribute('aria-hidden', 'true'); |
| 62 | + }); |
| 63 | + |
| 64 | + test('the narrow charset ciphers digits and leaves letters alone', async ({ page }) => { |
| 65 | + const sample = page |
| 66 | + .locator('app-config-lab .card') |
| 67 | + .filter({ hasText: 'Narrow charset' }) |
| 68 | + .locator('.config-sample'); |
| 69 | + |
| 70 | + const text = (await sample.textContent())!; |
| 71 | + |
| 72 | + // Letters are outside the cipher, so they survive verbatim in the DOM. |
| 73 | + expect(text).toContain('Order'); |
| 74 | + expect(text).toContain('shipped'); |
| 75 | + // The digits do not. |
| 76 | + expect(text).not.toContain('8391'); |
| 77 | + }); |
| 78 | +}); |
| 79 | + |
| 80 | +test.describe('prerender safety', () => { |
| 81 | + test('no lab instance renders during prerender', async ({ request }) => { |
| 82 | + // A child service running on the server overwrites the shell's seed in |
| 83 | + // TransferState, and the client then rebuilds the wrong cipher. |
| 84 | + const html = await (await request.get('./')).text(); |
| 85 | + |
| 86 | + expect(html).not.toContain('config-sample'); |
| 87 | + }); |
| 88 | +}); |
0 commit comments