diff --git a/packages/chess/e2e/modifier-profiles.spec.ts b/packages/chess/e2e/modifier-profiles.spec.ts index 39ad53f..09889a7 100644 --- a/packages/chess/e2e/modifier-profiles.spec.ts +++ b/packages/chess/e2e/modifier-profiles.spec.ts @@ -396,3 +396,306 @@ test.describe('Modifier Profiles — Lobby integration (T26)', () => { expect(selectedLabel).toContain('from link'); }); }); + +// ── T27: Final 6 — round out the vertical slice to 18 tests total ──── + +test.describe('Modifier Profiles — vertical slice (T27)', () => { + /** + * Library storage key — duplicated from `packages/chess/src/modifiers/library.ts` + * because the Playwright page context doesn't share our module graph. If + * `STORAGE_KEY` ever moves, both sites need the update. + */ + const LIBRARY_KEY = 'houserules:modifier-profiles:v1'; + + /** + * Build a `SavedModifierProfile` shape by hand. Kept here (not a shared + * helper) so the test file remains a single self-contained fixture — any + * future change to the shape surfaces as a compile error right next to + * the assertion, not three files away. + */ + function buildEntry(opts: { + id: string; + name: string; + starred?: boolean; + updatedAt?: number; + }): Record { + return { + id: opts.id, + name: opts.name, + profile: { + id: opts.id, + name: opts.name, + description: `T27 fixture: ${opts.name}`, + perType: [ + { + kind: 'hp-bonus', + pieceType: 'pawn', + color: 'white', + value: 1, + }, + ], + perInstance: [], + version: 1, + source: 'custom', + }, + starred: opts.starred ?? false, + updatedAt: opts.updatedAt ?? Date.now(), + }; + } + + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.evaluate((key) => { + localStorage.removeItem(key); + for (let i = localStorage.length - 1; i >= 0; i--) { + const k = localStorage.key(i); + if (k !== null && k.startsWith('paratype-chess:v2:autosave:')) { + localStorage.removeItem(k); + } + } + localStorage.removeItem('paratype-chess:v1:autosave'); + sessionStorage.clear(); + }, LIBRARY_KEY); + }); + + // T13 — Custom… option in the lobby picker opens the modifier editor. + // This documents the "Custom…" escape hatch as the canonical way for + // users to author a profile without first visiting a game. + test('lobby Custom… picker option opens the modifier editor', async ({ + page, + }) => { + await page.goto('/'); + const picker = page.getByTestId('profile-picker'); + await expect(picker).toBeVisible(); + + await picker.selectOption('custom'); + + // Editor modal appears; the picker stays on its previous value + // (selectedProfile=null → 'none'), which is a deliberate design + // choice so the UI doesn't falsely claim "Custom…" is active while + // the user is still editing. + await expect(page.getByTestId('modifier-editor-modal')).toBeVisible({ + timeout: 1500, + }); + }); + + // T14 — Profiles saved into localStorage survive a full page reload. + // This is a smoke test for the localStorage round-trip in + // `loadLibrary()` / `saveToLibrary()`: we seed, reload, then open the + // editor from a fresh game and confirm the library panel still + // enumerates the seeded entry. Reload-resilience is what makes + // "My Profiles" meaningful to users across browser sessions. + test('saved profile persists after page reload', async ({ page }) => { + const entry = buildEntry({ id: 't27-reload', name: 'Reload Test' }); + await page.evaluate( + ({ key, e }) => localStorage.setItem(key, JSON.stringify([e])), + { key: LIBRARY_KEY, e: entry }, + ); + + // Reload and navigate into a game so we can open the editor via + // the Rules drawer. + await page.reload(); + await page.locator('[data-action="play-solo"]').click(); + await page.waitForURL('**/game'); + await page.locator('[data-action="open-rules-drawer"]').click(); + await page.click('[data-testid="open-modifier-editor"]'); + await expect(page.getByTestId('modifier-editor-modal')).toBeVisible(); + + // The library panel should list the seeded entry. + await expect(page.getByTestId('profile-library-entry')).toBeVisible(); + await expect(page.getByTestId('profile-library-entry')).toContainText( + 'Reload Test', + ); + }); + + // T15 — Picker lists every entry from the library as an