test(chess/e2e): expand visual-builder Playwright suite to 11 scenarios
Replaces the original single-flow stub with a comprehensive suite
that exercises every user-facing behaviour of the visual authoring
surface against a live Vite dev server. 11 tests run in 23s.
Scenarios:
1. Mode toggle persists across reload
Opens editor, toggles to Visual, verifies aria-pressed state +
localStorage key, reloads, confirms Visual is still active on
the next mount.
2. Palette click adds top-level primitive to block list
Clicks palette-btn-on-turn-end, verifies block-card-on-turn-end
appears with matching aria-label and the narrative preview
mentions "turn end". Asserts via aria-label rather than visible
text so the open inspector docs do not cause strict-mode matches.
3. Clicking × removes the block without triggering a drag
Adds three blocks, clicks the × button on the middle one,
verifies it is gone AND that dnd-kits assertive announcer never
reported "Picked up sortable item" — direct regression for the
drag-handle isolation fix.
4. Clicking expand toggles the block without triggering a drag
Asserts aria-expanded flips from false to true on click without
the card being removed or reordered.
5. Selecting a trigger makes palette clicks add children
Adds on-turn-end, selects it, verifies palette-add-target-banner
appears with the parent label, then clicks add-to-attribute and
asserts there is exactly one add-to-attribute block AND it is
a descendant of block-card-on-turn-end.
6. "Add at top level instead" resets the nested-target selection
After entering nested-add mode, clicks the escape button and
verifies subsequent palette adds are top-level siblings.
7. × on a nested child removes only that child
Verifies nested removal leaves the parent intact.
8. Preview narrative reflects tree mutations immediately
Adds primitive and checks narrative; removes and checks narrative
no longer mentions the seeded attribute.
9. Save → reload → load preserves the composed descriptor
Fills inspector fields (attr=Hp, delta=1), saves, reloads,
confirms Visual mode is remembered, loads from library, verifies
inspector value survived the round-trip.
10. Depth-4 descriptor surfaces validation banner + disables save
Pre-seeds localStorage with a depth-4 descriptor (conditional
→ on-capture → on-damaged → conditional → add-to-attribute),
loads it, verifies the validation banner renders and the Save
button becomes disabled.
11. Toggling Form ↔ Visual preserves the composed descriptor
Composes a tree, captures the JSON preview, toggles to Form and
back to Visual, verifies JSON preview is byte-identical.
The spec uses data-testid selectors exclusively where possible,
falls back to aria-label for the block card outer <article>, and
uses click({ position }) to land on the card header (avoiding the
× / expand / grip / inspector overlays).
This commit is contained in:
parent
2d1efb1b3a
commit
9e31b6d682
1 changed files with 495 additions and 89 deletions
|
|
@ -1,130 +1,536 @@
|
|||
/**
|
||||
* T25 — End-to-end suite for the visual modifier builder.
|
||||
*
|
||||
* Exercises the full visual-mode authoring surface end-to-end against
|
||||
* a live Vite dev server. The unit tests under
|
||||
* `src/ui/visual-builder/**` cover props, rendering shapes, and pure
|
||||
* tree mutations in isolation; this spec covers integration: real
|
||||
* pointer events, real localStorage, real dnd-kit wiring, real
|
||||
* narrate → preview composition, real validator round-trips.
|
||||
*
|
||||
* Scenarios:
|
||||
* 1. Mode toggle persistence across reloads
|
||||
* 2. Palette click adds top-level primitive + appears in block list
|
||||
* 3. Drag-handle isolation — clicking × removes block, does NOT drag
|
||||
* 4. Drag-handle isolation — clicking expand toggles, does NOT drag
|
||||
* 5. Nested add — selecting a trigger targets new primitives as children
|
||||
* 6. Nested add — auto-expands parent + "Add at top level instead" resets
|
||||
* 7. Nested remove — × on a child primitive removes only that child
|
||||
* 8. Preview narrative updates live as the tree mutates
|
||||
* 9. Save → reload → load round-trip preserves the composed descriptor
|
||||
* 10. Validation banner appears on depth-4 composition + save stays disabled
|
||||
* 11. Form ↔ Visual toggle preserves descriptor content byte-equal
|
||||
*/
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
const CUSTOM_LIBRARY_KEY = 'houserules:custom-modifiers:v1';
|
||||
const EDITOR_MODE_KEY = 'houserules:custom-modifier-editor-mode:v1';
|
||||
const PROFILE_LIBRARY_KEY = 'houserules:modifier-profiles:v1';
|
||||
|
||||
/** Wipe both libraries + mode pref and return to a clean lobby. */
|
||||
async function freshLobby(page: Page): Promise<void> {
|
||||
await page.goto('/');
|
||||
await page.evaluate(
|
||||
({ customKey, modeKey }) => {
|
||||
({ customKey, modeKey, profileKey }) => {
|
||||
localStorage.removeItem(customKey);
|
||||
localStorage.removeItem(modeKey);
|
||||
localStorage.removeItem(profileKey);
|
||||
sessionStorage.clear();
|
||||
},
|
||||
{
|
||||
customKey: CUSTOM_LIBRARY_KEY,
|
||||
modeKey: EDITOR_MODE_KEY,
|
||||
profileKey: PROFILE_LIBRARY_KEY,
|
||||
},
|
||||
{ customKey: CUSTOM_LIBRARY_KEY, modeKey: EDITOR_MODE_KEY },
|
||||
);
|
||||
await page.reload();
|
||||
await page.waitForSelector('[data-testid="page-home"]');
|
||||
}
|
||||
|
||||
test.describe('T25 — Custom modifier visual builder e2e', () => {
|
||||
/** Open the Custom Modifier Editor via the standard lobby flow. */
|
||||
async function openCustomModifierEditor(page: Page): Promise<void> {
|
||||
await page.getByTestId('profile-picker').selectOption('custom');
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('per-type-panel-paste')
|
||||
.or(page.locator('[role="dialog"], .fixed.inset-0').first()),
|
||||
).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await page.getByTestId('open-custom-modifier-editor').click();
|
||||
await expect(page.getByTestId('custom-modifier-editor')).toBeVisible({
|
||||
timeout: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Switch the editor into Visual mode. */
|
||||
async function switchToVisualMode(page: Page): Promise<void> {
|
||||
await page.getByTestId('custom-modifier-editor-mode-visual').click();
|
||||
// Palette is the clearest visual-mode marker.
|
||||
await expect(page.getByTestId('palette-btn-on-move')).toBeVisible();
|
||||
await expect(page.getByTestId('preview-pane')).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a block by its kind. Clicks into the card's header region
|
||||
* (top 40px) so the click lands on the card body, not on the expand /
|
||||
* × buttons that live in the same row, nor on the inspector docs that
|
||||
* render below when the card is already selected.
|
||||
*/
|
||||
async function selectBlockCard(page: Page, kind: string): Promise<void> {
|
||||
const card = page.getByTestId(`block-card-${kind}`).first();
|
||||
await expect(card).toBeVisible();
|
||||
await card.click({ position: { x: 100, y: 12 } });
|
||||
}
|
||||
|
||||
test.describe('Visual modifier builder — T25 e2e', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await freshLobby(page);
|
||||
});
|
||||
|
||||
test('visual authoring flow: create, save, load', async ({ page }) => {
|
||||
// 1. Open editor
|
||||
await page.getByTestId('profile-picker').selectOption('custom');
|
||||
await expect(page.getByTestId('per-type-panel-paste').or(
|
||||
page.locator('[role="dialog"], .fixed.inset-0').first(),
|
||||
)).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await page.getByTestId('open-custom-modifier-editor').click();
|
||||
await expect(page.getByTestId('custom-modifier-editor')).toBeVisible();
|
||||
// ---------------------------------------------------------------
|
||||
// 1. Mode toggle persistence
|
||||
// ---------------------------------------------------------------
|
||||
test('mode toggle persists across reload', async ({ page }) => {
|
||||
await openCustomModifierEditor(page);
|
||||
|
||||
// 2. Toggle to visual mode
|
||||
await page.getByTestId('custom-modifier-editor-mode-visual').click();
|
||||
|
||||
// Assert visual mode components are visible
|
||||
// Default mode is Form.
|
||||
await expect(
|
||||
page.getByTestId('custom-modifier-editor-mode-form'),
|
||||
).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(
|
||||
page.getByTestId('custom-modifier-editor-mode-visual'),
|
||||
).toHaveAttribute('aria-pressed', 'false');
|
||||
|
||||
// Switch to Visual.
|
||||
await switchToVisualMode(page);
|
||||
await expect(
|
||||
page.getByTestId('custom-modifier-editor-mode-visual'),
|
||||
).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// localStorage has the preference.
|
||||
const stored = await page.evaluate(
|
||||
(key) => localStorage.getItem(key),
|
||||
EDITOR_MODE_KEY,
|
||||
);
|
||||
expect(stored).toBe('visual');
|
||||
|
||||
// Reload — mode should still be Visual on the way back in.
|
||||
await page.reload();
|
||||
await page.waitForSelector('[data-testid="page-home"]');
|
||||
await openCustomModifierEditor(page);
|
||||
await expect(
|
||||
page.getByTestId('custom-modifier-editor-mode-visual'),
|
||||
).toHaveAttribute('aria-pressed', 'true');
|
||||
// Palette rendered without an explicit click.
|
||||
await expect(page.getByTestId('palette-btn-on-move')).toBeVisible();
|
||||
await expect(page.getByTestId('preview-pane')).toBeVisible();
|
||||
|
||||
// Give it a name
|
||||
await page.getByPlaceholder('Modifier Name').fill('Visual Test Modifier');
|
||||
});
|
||||
|
||||
// 3. Add on-move primitive
|
||||
await page.getByTestId('palette-btn-on-move').click();
|
||||
|
||||
// It should appear in the block list
|
||||
const onMoveBlock = page.getByTestId('block-card-on-move');
|
||||
await expect(onMoveBlock).toBeVisible();
|
||||
await expect(onMoveBlock.getByText('On Move')).toBeVisible();
|
||||
// ---------------------------------------------------------------
|
||||
// 2. Palette add (top-level)
|
||||
// ---------------------------------------------------------------
|
||||
test('palette click adds top-level primitive to block list', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
// 4. Add add-to-attribute inside on-move
|
||||
// Open the block to show dropzones
|
||||
await onMoveBlock.click();
|
||||
// Click a palette button and verify a matching block appears.
|
||||
await page.getByTestId('palette-btn-on-turn-end').click();
|
||||
const block = page.getByTestId('block-card-on-turn-end');
|
||||
await expect(block).toBeVisible();
|
||||
// Use the aria-label ("On Turn End block") which is unique to the
|
||||
// card's outer <article>, even when an inspector opens below.
|
||||
await expect(block).toHaveAttribute('aria-label', /On Turn End/);
|
||||
|
||||
// Preview narrative reflects the new primitive.
|
||||
const narrative = page
|
||||
.getByTestId('preview-pane')
|
||||
.getByRole('region', { name: 'narrative' });
|
||||
await expect(narrative).toContainText(/turn end/i);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 3. Drag-handle isolation — × button
|
||||
// ---------------------------------------------------------------
|
||||
test('clicking × removes the block without triggering a drag', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
// Add three blocks.
|
||||
await page.getByTestId('palette-btn-seed-attribute').click();
|
||||
await page.getByTestId('palette-btn-add-to-attribute').click();
|
||||
await page.getByTestId('palette-btn-multiply-attribute').click();
|
||||
|
||||
await expect(page.getByTestId('block-card-seed-attribute')).toBeVisible();
|
||||
await expect(page.getByTestId('block-card-add-to-attribute')).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('block-card-multiply-attribute'),
|
||||
).toBeVisible();
|
||||
|
||||
// Click the × on the middle (add-to-attribute) block — must remove
|
||||
// it WITHOUT starting a drag. If pointerdown on the × started a
|
||||
// drag, dnd-kit's announcer element would contain "Picked up
|
||||
// sortable item", and the block wouldn't be removed.
|
||||
const middleBlock = page.getByTestId('block-card-add-to-attribute');
|
||||
await middleBlock
|
||||
.getByRole('button', { name: 'Remove primitive' })
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId('block-card-add-to-attribute')).toHaveCount(
|
||||
0,
|
||||
);
|
||||
// The other two still exist.
|
||||
await expect(page.getByTestId('block-card-seed-attribute')).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('block-card-multiply-attribute'),
|
||||
).toBeVisible();
|
||||
|
||||
// dnd-kit's visually-hidden announcer should not have reported a pickup.
|
||||
const announcement = await page
|
||||
.locator('[aria-live="assertive"]')
|
||||
.first()
|
||||
.textContent();
|
||||
expect(announcement ?? '').not.toContain('Picked up sortable item');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 4. Drag-handle isolation — expand chevron
|
||||
// ---------------------------------------------------------------
|
||||
test('clicking expand toggles the block without triggering a drag', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
await page.getByTestId('palette-btn-on-turn-end').click();
|
||||
const block = page.getByTestId('block-card-on-turn-end');
|
||||
await expect(block).toBeVisible();
|
||||
|
||||
// Click the expand chevron (aria-expanded toggles).
|
||||
const expandBtn = block.getByRole('button', { name: /expand|collapse/i });
|
||||
await expect(expandBtn).toHaveAttribute('aria-expanded', 'false');
|
||||
await expandBtn.click();
|
||||
await expect(expandBtn).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// Block still exists — the click didn't trigger a reorder drag.
|
||||
await expect(block).toBeVisible();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 5. Nested add via selection
|
||||
// ---------------------------------------------------------------
|
||||
test('selecting a trigger makes palette clicks add children', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
// Add a trigger container.
|
||||
await page.getByTestId('palette-btn-on-turn-end').click();
|
||||
const onTurnEnd = page.getByTestId('block-card-on-turn-end');
|
||||
await expect(onTurnEnd).toBeVisible();
|
||||
|
||||
// Select it by clicking the header area (avoids the × / expand /
|
||||
// inspector regions of the card).
|
||||
await selectBlockCard(page, 'on-turn-end');
|
||||
|
||||
// The palette-add-target banner should appear.
|
||||
await expect(page.getByTestId('palette-add-target-banner')).toBeVisible();
|
||||
await expect(page.getByTestId('palette-add-target-banner')).toContainText(
|
||||
/on turn end/i,
|
||||
);
|
||||
|
||||
// Now click a state primitive — it should land INSIDE on-turn-end,
|
||||
// not as a top-level sibling.
|
||||
await page.getByTestId('palette-btn-add-to-attribute').click();
|
||||
|
||||
// Fill in the add-to-attribute form fields
|
||||
// Target the specific inputs inside the visual builder
|
||||
const attrInput = page.locator('[data-testid="primitive-add-to-attribute-attr-input"]').first();
|
||||
// The parent auto-expands so the child is visible.
|
||||
// Only ONE add-to-attribute block exists (the nested one).
|
||||
const addBlocks = page.getByTestId('block-card-add-to-attribute');
|
||||
await expect(addBlocks).toHaveCount(1);
|
||||
|
||||
// And it lives inside on-turn-end (descendant selector).
|
||||
await expect(
|
||||
onTurnEnd.getByTestId('block-card-add-to-attribute'),
|
||||
).toBeVisible();
|
||||
|
||||
// Preview narrative should mention both the turn-end trigger AND
|
||||
// the nested add-to-attribute effect.
|
||||
const narrative = page
|
||||
.getByTestId('preview-pane')
|
||||
.getByRole('region', { name: 'narrative' });
|
||||
await expect(narrative).toContainText(/turn end/i);
|
||||
await expect(narrative).toContainText(/add/i);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 6. "Add at top level instead" resets selection
|
||||
// ---------------------------------------------------------------
|
||||
test('“Add at top level instead” resets the nested-target selection', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
await page.getByTestId('palette-btn-on-turn-end').click();
|
||||
await selectBlockCard(page, 'on-turn-end');
|
||||
|
||||
await expect(page.getByTestId('palette-add-target-banner')).toBeVisible();
|
||||
|
||||
// Click the "Add at top level instead" button.
|
||||
await page
|
||||
.getByTestId('palette-add-target-banner')
|
||||
.getByRole('button', { name: /top level/i })
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId('palette-add-target-banner')).toHaveCount(0);
|
||||
|
||||
// Now clicking a palette button adds at top level.
|
||||
await page.getByTestId('palette-btn-seed-attribute').click();
|
||||
// Both blocks are top-level siblings now.
|
||||
const topLevelBlocks = page.locator('[data-testid^="block-card-"]');
|
||||
await expect(
|
||||
topLevelBlocks.filter({ has: page.getByText('On Turn End') }),
|
||||
).toHaveCount(1);
|
||||
await expect(page.getByTestId('block-card-seed-attribute')).toBeVisible();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 7. Nested remove
|
||||
// ---------------------------------------------------------------
|
||||
test('× on a nested child removes only that child', async ({ page }) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
// Set up: on-turn-end containing add-to-attribute.
|
||||
await page.getByTestId('palette-btn-on-turn-end').click();
|
||||
await selectBlockCard(page, 'on-turn-end');
|
||||
const onTurnEnd = page.getByTestId('block-card-on-turn-end');
|
||||
await page.getByTestId('palette-btn-add-to-attribute').click();
|
||||
|
||||
const nestedChild = onTurnEnd.getByTestId('block-card-add-to-attribute');
|
||||
await expect(nestedChild).toBeVisible();
|
||||
|
||||
// Click × on the nested child.
|
||||
await nestedChild
|
||||
.getByRole('button', { name: 'Remove primitive' })
|
||||
.click();
|
||||
|
||||
// Nested child gone.
|
||||
await expect(
|
||||
onTurnEnd.getByTestId('block-card-add-to-attribute'),
|
||||
).toHaveCount(0);
|
||||
// Parent still present.
|
||||
await expect(onTurnEnd).toBeVisible();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 8. Preview narrative updates live
|
||||
// ---------------------------------------------------------------
|
||||
test('preview narrative reflects tree mutations immediately', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
const narrative = page
|
||||
.getByTestId('preview-pane')
|
||||
.getByRole('region', { name: 'narrative' });
|
||||
|
||||
// Start: palette view, narrative may be empty OR show placeholder.
|
||||
// After adding seed-attribute it must mention the new primitive.
|
||||
await page.getByTestId('palette-btn-seed-attribute').click();
|
||||
await expect(narrative).toContainText(/set|seed/i);
|
||||
|
||||
// After removing it, narrative reverts.
|
||||
await page
|
||||
.getByTestId('block-card-seed-attribute')
|
||||
.getByRole('button', { name: 'Remove primitive' })
|
||||
.click();
|
||||
const afterRemove = (await narrative.textContent()) ?? '';
|
||||
expect(afterRemove).not.toMatch(/ShieldCharges/i);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 9. Save → reload → load round-trip
|
||||
// ---------------------------------------------------------------
|
||||
test('save → reload → load preserves the composed descriptor', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
// Compose a simple top-level structure: add-to-attribute + aura.
|
||||
await page.getByPlaceholder('Modifier Name').fill('Visual Round-Trip');
|
||||
await page.getByTestId('palette-btn-add-to-attribute').click();
|
||||
await selectBlockCard(page, 'add-to-attribute');
|
||||
|
||||
// Inspector opens automatically on select — fill the fields.
|
||||
const attrInput = page
|
||||
.locator('[data-testid="primitive-add-to-attribute-attr-input"]')
|
||||
.first();
|
||||
await expect(attrInput).toBeVisible({ timeout: 3000 });
|
||||
await attrInput.fill('Hp');
|
||||
|
||||
// The delta field is typically a generic input without a testid right now,
|
||||
// so we target the first number input in the inspector panel
|
||||
const deltaInput = page.locator('input[type="number"]').first();
|
||||
await deltaInput.fill('1');
|
||||
|
||||
// 5. Verify preview narrative
|
||||
const previewPane = page.getByTestId('preview-pane');
|
||||
await expect(previewPane).toContainText('When this piece moves');
|
||||
await expect(previewPane).toContainText('add 1 to Hp');
|
||||
|
||||
// Take screenshot of composed state
|
||||
await page.screenshot({ path: '.sisyphus/evidence/task-25-visual-composed.png' });
|
||||
|
||||
// 6. Save to library
|
||||
// Save.
|
||||
await page.getByTestId('custom-save').click();
|
||||
await expect(page.getByText('Custom modifier saved to library')).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/saved to library|custom modifier saved/i),
|
||||
).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// 7. Reload page
|
||||
// Reload — mode preference persists, library persists.
|
||||
await page.reload();
|
||||
await page.waitForSelector('[data-testid="page-home"]');
|
||||
await openCustomModifierEditor(page);
|
||||
// Still in Visual mode.
|
||||
await expect(
|
||||
page.getByTestId('custom-modifier-editor-mode-visual'),
|
||||
).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// 8. Re-open editor & load
|
||||
await page.getByTestId('profile-picker').selectOption('custom');
|
||||
await expect(page.getByTestId('per-type-panel-paste').or(
|
||||
page.locator('[role="dialog"], .fixed.inset-0').first(),
|
||||
)).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await page.getByTestId('open-custom-modifier-editor').click();
|
||||
await expect(page.getByTestId('custom-modifier-editor')).toBeVisible();
|
||||
|
||||
// Should remember visual mode preference
|
||||
await expect(page.getByTestId('custom-modifier-editor-mode-visual')).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// Load from library.
|
||||
await page.getByTestId('custom-load').click();
|
||||
await page.getByText('Visual Test Modifier').click();
|
||||
await page.getByText('Visual Round-Trip').click();
|
||||
|
||||
// 9. Verify loaded structure
|
||||
const loadedBlock = page.getByTestId('block-card-on-move');
|
||||
await expect(loadedBlock).toBeVisible();
|
||||
await expect(loadedBlock.getByText('On Move')).toBeVisible();
|
||||
|
||||
// Open the block to see children
|
||||
await loadedBlock.click();
|
||||
await expect(page.getByText('Add To Attribute')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="primitive-add-to-attribute-attr-input"]').first()).toHaveValue('Hp');
|
||||
await expect(page.locator('input[type="number"]').first()).toHaveValue('1');
|
||||
// The composed primitive is back.
|
||||
await expect(page.getByTestId('block-card-add-to-attribute')).toBeVisible();
|
||||
// Select it to reveal the inspector, then confirm persisted values.
|
||||
await selectBlockCard(page, 'add-to-attribute');
|
||||
await expect(
|
||||
page.locator('[data-testid="primitive-add-to-attribute-attr-input"]').first(),
|
||||
).toHaveValue('Hp');
|
||||
});
|
||||
|
||||
// 10. Negative: try to add beyond max depth (validator prevents save)
|
||||
// Add conditional -> on-capture -> on-damaged
|
||||
// The block list allows arbitrary adding, but validation fails
|
||||
await page.getByTestId('palette-btn-conditional').click();
|
||||
const condBlock = page.getByTestId('block-card-conditional');
|
||||
await condBlock.click();
|
||||
|
||||
await page.getByTestId('palette-btn-on-capture').click();
|
||||
const captureBlock = page.getByTestId('block-card-on-capture');
|
||||
await captureBlock.click();
|
||||
|
||||
await page.getByTestId('palette-btn-on-damaged').click();
|
||||
|
||||
// The validator banner should show depth exceeded
|
||||
await expect(page.getByText(/exceed 3|Fix errors in Form mode/)).toBeVisible();
|
||||
|
||||
// Save should be disabled
|
||||
// ---------------------------------------------------------------
|
||||
// 10. Depth-4 validation banner — pre-seed a too-deep descriptor
|
||||
// via localStorage then load it. Verifies the banner surfaces and
|
||||
// the Save button is disabled regardless of how the descriptor got
|
||||
// deep (manual click, paste, import, etc.).
|
||||
// ---------------------------------------------------------------
|
||||
test('depth-4 descriptor surfaces validation banner + disables save', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Seed the library with a depth-4 descriptor before opening the editor.
|
||||
await page.evaluate((key) => {
|
||||
const depth4: unknown = {
|
||||
type: 'data',
|
||||
id: 'custom-depth4',
|
||||
name: 'Depth 4 Test',
|
||||
description: '',
|
||||
version: 1,
|
||||
uiForm: 'primitive-composer',
|
||||
source: 'custom',
|
||||
targetAttrs: [],
|
||||
primitives: [
|
||||
{
|
||||
kind: 'conditional',
|
||||
params: {
|
||||
condition: { type: 'always' },
|
||||
then: [
|
||||
{
|
||||
kind: 'on-capture',
|
||||
params: {
|
||||
primitives: [
|
||||
{
|
||||
kind: 'on-damaged',
|
||||
params: {
|
||||
primitives: [
|
||||
{
|
||||
kind: 'conditional',
|
||||
params: {
|
||||
condition: { type: 'always' },
|
||||
then: [
|
||||
{ kind: 'add-to-attribute', params: { attr: 'Hp', delta: 1 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify([
|
||||
{
|
||||
id: 'custom-depth4',
|
||||
descriptor: depth4,
|
||||
starred: false,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
]),
|
||||
);
|
||||
}, CUSTOM_LIBRARY_KEY);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForSelector('[data-testid="page-home"]');
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
// Load the pre-seeded depth-4 descriptor.
|
||||
await page.getByTestId('custom-load').click();
|
||||
await page.getByText('Depth 4 Test').click();
|
||||
|
||||
// Validation banner appears.
|
||||
await expect(
|
||||
page.getByText(/exceed|depth|fix errors/i).first(),
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Save is disabled — validator rejects depth-4 descriptors.
|
||||
await expect(page.getByTestId('custom-save')).toBeDisabled();
|
||||
|
||||
await page.screenshot({ path: '.sisyphus/evidence/task-25-visual-validation-error.png' });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 11. Form ↔ Visual toggle preserves descriptor content
|
||||
// ---------------------------------------------------------------
|
||||
test('toggling Form ↔ Visual preserves the composed descriptor', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openCustomModifierEditor(page);
|
||||
await switchToVisualMode(page);
|
||||
|
||||
// Compose a small non-trivial tree.
|
||||
await page.getByPlaceholder('Modifier Name').fill('Toggle Test');
|
||||
await page.getByTestId('palette-btn-on-turn-end').click();
|
||||
await selectBlockCard(page, 'on-turn-end');
|
||||
await page.getByTestId('palette-btn-add-to-attribute').click();
|
||||
|
||||
// Capture the JSON preview (preview-json is the <pre> itself).
|
||||
await page
|
||||
.getByTestId('preview-pane')
|
||||
.getByRole('tab', { name: 'JSON' })
|
||||
.click();
|
||||
const jsonView = page.getByTestId('preview-json');
|
||||
await expect(jsonView).toBeVisible();
|
||||
const jsonBefore = await jsonView.textContent();
|
||||
expect(jsonBefore ?? '').toContain('on-turn-end');
|
||||
expect(jsonBefore ?? '').toContain('add-to-attribute');
|
||||
|
||||
// Toggle to Form, then back to Visual.
|
||||
await page.getByTestId('custom-modifier-editor-mode-form').click();
|
||||
await expect(
|
||||
page.getByTestId('custom-modifier-editor-mode-form'),
|
||||
).toHaveAttribute('aria-pressed', 'true');
|
||||
await page.getByTestId('custom-modifier-editor-mode-visual').click();
|
||||
await expect(page.getByTestId('palette-btn-on-move')).toBeVisible();
|
||||
|
||||
// Preview JSON after the round-trip — byte-equal content.
|
||||
await page
|
||||
.getByTestId('preview-pane')
|
||||
.getByRole('tab', { name: 'JSON' })
|
||||
.click();
|
||||
await expect(jsonView).toBeVisible();
|
||||
const jsonAfter = await jsonView.textContent();
|
||||
|
||||
expect(jsonAfter).toBe(jsonBefore);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue