feat(modifiers): bundle preset activations on profiles + editor Presets tab
Profiles can now carry a list of PresetActivation entries alongside their per-type / per-instance modifiers. When such a profile is picked in the Lobby, its bundled presets are unioned with any layout suggestedPresets (profile config wins on id-collision). Changes: - ModifierProfile.presetActivations added (optional readonly array). Zod schema + server wire schema mirror the field; drift guard picks up forgotten updates on either side. - ModifierProfileEditor grows a Presets tab in the right column sharing space with the Library tab. Each preset row carries a checkbox, scope radio (both/white/black), and a turns counter (blank = permanent). Inline diagnostics warn on redundant layout overlap and on loose-scope incompatibility; hard errors under overlapping scope disable Save. - Lobby merges profile.presetActivations into its active preset set on profile select, on editor close, and on URL deep-link. - LayoutPicker suggested-preset chips now expose aria-pressed so the active state is readable by assistive tech + e2e tests. Tests: library round-trip preserves presetActivations (unit); two new e2e scenarios (pre-seeded bundled profile auto-activates; full editor-author loop persists + reflects in the lobby). 1752 unit + 105 playwright all green.
This commit is contained in:
parent
3bdfba38e0
commit
699c288a98
9 changed files with 602 additions and 12 deletions
|
|
@ -158,6 +158,101 @@ test.describe('Modifier profile ⇄ layout binding', () => {
|
|||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('profile with bundled presetActivations auto-activates them in the Lobby', async ({
|
||||
page,
|
||||
}) => {
|
||||
await freshLobby(page);
|
||||
// Seed a profile bound to the pawns-only layout (which suggests
|
||||
// first-promotion-wins) AND bundling that same preset. This lets
|
||||
// us verify the chip flips to active purely from the profile
|
||||
// selection — no user-tap on the suggested-preset chip involved.
|
||||
await page.evaluate(
|
||||
({ key }) => {
|
||||
const entry = {
|
||||
id: 'lib-with-preset',
|
||||
name: 'FPW Profile',
|
||||
profile: {
|
||||
id: 'lib-with-preset',
|
||||
name: 'FPW Profile',
|
||||
description: '',
|
||||
layoutId: 'pawns-only',
|
||||
perType: [],
|
||||
perInstance: [],
|
||||
presetActivations: [
|
||||
{
|
||||
id: 'first-promotion-wins',
|
||||
scope: 'both',
|
||||
turnsRemaining: null,
|
||||
},
|
||||
],
|
||||
version: 1,
|
||||
source: 'custom',
|
||||
},
|
||||
starred: false,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
localStorage.setItem(key, JSON.stringify([entry]));
|
||||
},
|
||||
{ key: PROFILE_LIBRARY_KEY },
|
||||
);
|
||||
await page.reload();
|
||||
|
||||
await page.getByTestId('profile-picker').selectOption('lib-with-preset');
|
||||
|
||||
// Layout auto-snaps to pawns-only (layoutId binding, already
|
||||
// covered elsewhere but confirmed here for precondition).
|
||||
await expect(page.getByTestId('layout-picker')).toHaveValue('pawns-only');
|
||||
|
||||
// The layout's suggested-preset chip for first-promotion-wins
|
||||
// should now be pressed because the profile's
|
||||
// presetActivations entry populated the Lobby's `presets` state.
|
||||
const chip = page.getByTestId(
|
||||
'layout-suggested-preset-first-promotion-wins',
|
||||
);
|
||||
await expect(chip).toBeVisible();
|
||||
await expect(chip).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
|
||||
test('editor bundles a preset on save, which auto-activates on reselect', async ({
|
||||
page,
|
||||
}) => {
|
||||
await freshLobby(page);
|
||||
|
||||
// Open the editor via the picker's "Custom…" option.
|
||||
await page.getByTestId('profile-picker').selectOption('custom');
|
||||
await expect(page.getByTestId('modifier-editor-modal')).toBeVisible({
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
// Set name, bind the pawns-only layout, switch to the Presets tab.
|
||||
await page.getByTestId('profile-name').fill('Bundled Presets Test');
|
||||
await page.getByTestId('bound-layout-picker').selectOption('pawns-only');
|
||||
await page.getByTestId('right-tab-presets').click();
|
||||
await expect(page.getByTestId('preset-panel')).toBeVisible();
|
||||
|
||||
// Toggle the first-promotion-wins preset on.
|
||||
await page
|
||||
.getByTestId('preset-panel-toggle-first-promotion-wins')
|
||||
.check();
|
||||
// Scope defaults to "both" (the toggle button exposes aria-checked).
|
||||
await expect(
|
||||
page.getByTestId('preset-panel-scope-first-promotion-wins-both'),
|
||||
).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
// Save + close.
|
||||
await page.getByTestId('save-profile').click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// Back in the lobby: the suggested-preset chip for the bundled
|
||||
// rule shows as pressed because the editor-close handler applied
|
||||
// the profile's bundled presets to the Lobby's active set.
|
||||
const chip = page.getByTestId(
|
||||
'layout-suggested-preset-first-promotion-wins',
|
||||
);
|
||||
await expect(chip).toBeVisible();
|
||||
await expect(chip).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
|
||||
test('editor persists the bound layout on save', async ({ page }) => {
|
||||
await freshLobby(page);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue