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).
536 lines
20 KiB
TypeScript
536 lines
20 KiB
TypeScript
/**
|
||
* 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, profileKey }) => {
|
||
localStorage.removeItem(customKey);
|
||
localStorage.removeItem(modeKey);
|
||
localStorage.removeItem(profileKey);
|
||
sessionStorage.clear();
|
||
},
|
||
{
|
||
customKey: CUSTOM_LIBRARY_KEY,
|
||
modeKey: EDITOR_MODE_KEY,
|
||
profileKey: PROFILE_LIBRARY_KEY,
|
||
},
|
||
);
|
||
await page.reload();
|
||
await page.waitForSelector('[data-testid="page-home"]');
|
||
}
|
||
|
||
/** 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);
|
||
});
|
||
|
||
// ---------------------------------------------------------------
|
||
// 1. Mode toggle persistence
|
||
// ---------------------------------------------------------------
|
||
test('mode toggle persists across reload', async ({ page }) => {
|
||
await openCustomModifierEditor(page);
|
||
|
||
// 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();
|
||
});
|
||
|
||
// ---------------------------------------------------------------
|
||
// 2. Palette add (top-level)
|
||
// ---------------------------------------------------------------
|
||
test('palette click adds top-level primitive to block list', async ({
|
||
page,
|
||
}) => {
|
||
await openCustomModifierEditor(page);
|
||
await switchToVisualMode(page);
|
||
|
||
// 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();
|
||
|
||
// 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');
|
||
const deltaInput = page.locator('input[type="number"]').first();
|
||
await deltaInput.fill('1');
|
||
|
||
// Save.
|
||
await page.getByTestId('custom-save').click();
|
||
await expect(
|
||
page.getByText(/saved to library|custom modifier saved/i),
|
||
).toBeVisible({ timeout: 3000 });
|
||
|
||
// 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');
|
||
|
||
// Load from library.
|
||
await page.getByTestId('custom-load').click();
|
||
await page.getByText('Visual Round-Trip').click();
|
||
|
||
// 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. 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();
|
||
});
|
||
|
||
// ---------------------------------------------------------------
|
||
// 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);
|
||
});
|
||
});
|