Major update for v1 and tests
This commit is contained in:
138
e2e/text-editing.spec.js
Normal file
138
e2e/text-editing.spec.js
Normal file
@@ -0,0 +1,138 @@
|
||||
// E2E: text element CRUD via the Text tab.
|
||||
//
|
||||
// What this guards
|
||||
// ────────────────
|
||||
// The most common editing flow in the app: open the Text tab, type a
|
||||
// message, add it to the canvas, optionally edit or delete it.
|
||||
// Vitest covers each piece (TextTab's local state, useDesignEditor's
|
||||
// addElement, LayersPanel rendering) but not the whole pipeline as
|
||||
// the user experiences it. This spec runs the actual integration.
|
||||
//
|
||||
// Why E2E rather than RTL component tests
|
||||
// ───────────────────────────────────────
|
||||
// TextTab is leaf-ish but composed with Sidebar, App's selectedElement
|
||||
// logic, and useDesignEditor — testing it in RTL would require either
|
||||
// mocking half the app (brittle) or rendering the real App with jsdom
|
||||
// (which then breaks on Konva, because jsdom doesn't have a real
|
||||
// canvas). Playwright drives the actual Chromium where everything
|
||||
// works, including Konva.
|
||||
//
|
||||
// Selector strategy
|
||||
// ─────────────────
|
||||
// We use accessible queries throughout — getByRole, getByLabel,
|
||||
// getByText. CSS class selectors would be tighter to the
|
||||
// implementation and break on any styling refactor; the accessible
|
||||
// roles describe what the user is looking at, which is stable across
|
||||
// styling changes. The cost is occasional verbosity (`getByRole('tab',
|
||||
// { name: 'Text' })` vs `.tab-text`) but the test stays valid as
|
||||
// styles iterate.
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
gotoFreshEditor,
|
||||
addTextViaSidebar,
|
||||
getElementsCount,
|
||||
} from './fixtures/editor.js';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Every test starts on the editor with empty state. See
|
||||
// gotoFreshEditor's docblock for the full reset sequence —
|
||||
// localStorage + service workers + caches all wiped.
|
||||
await gotoFreshEditor(page);
|
||||
});
|
||||
|
||||
test('starts with zero elements', async ({ page }) => {
|
||||
// Empty-state check. The LayersPanel renders a banner instead of
|
||||
// a title when there are no elements, so getElementsCount returns
|
||||
// 0 by checking for the title's absence.
|
||||
expect(await getElementsCount(page)).toBe(0);
|
||||
// The empty-state message is the canonical "no layers" indicator.
|
||||
// We assert it's visible to confirm we're really on an empty
|
||||
// editor — a partially-loaded editor without elements would also
|
||||
// have a 0 count but no empty-state banner.
|
||||
await expect(page.getByText('No elements yet')).toBeVisible();
|
||||
});
|
||||
|
||||
test('adds a text element via the sidebar and shows it in the Layers panel', async ({ page }) => {
|
||||
// The headline text-editing flow. addTextViaSidebar drives the
|
||||
// Text tab → fill textarea → click Add → wait for the layer count
|
||||
// to increment. After it returns, we make user-observable
|
||||
// assertions about what's on the canvas.
|
||||
await addTextViaSidebar(page, { text: 'Hello world' });
|
||||
|
||||
// Count is now 1.
|
||||
expect(await getElementsCount(page)).toBe(1);
|
||||
|
||||
// The LayersPanel row for the new element. LayersPanel uses
|
||||
// `getName(el)` which returns the text content for text elements,
|
||||
// so "Hello world" should appear as the row label.
|
||||
await expect(page.locator('.layers-item-name', { hasText: 'Hello world' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('adding two text elements lists both in the Layers panel, newest on top', async ({ page }) => {
|
||||
// LayersPanel docblock: "Lists all elements on the canvas in
|
||||
// render order (last in array = topmost)." The panel renders
|
||||
// top-down with the most recently added on top, mirroring how
|
||||
// users mentally stack layers in design tools.
|
||||
await addTextViaSidebar(page, { text: 'First' });
|
||||
await addTextViaSidebar(page, { text: 'Second' });
|
||||
|
||||
expect(await getElementsCount(page)).toBe(2);
|
||||
|
||||
// Order check: read all row names and verify "Second" precedes
|
||||
// "First". allTextContents resolves to an array in DOM order;
|
||||
// for a flex column that's top-to-bottom.
|
||||
const names = await page.locator('.layers-item-name').allTextContents();
|
||||
expect(names.indexOf('Second')).toBeLessThan(names.indexOf('First'));
|
||||
});
|
||||
|
||||
test('editing the textarea after selecting a text element updates the layer name', async ({ page }) => {
|
||||
// Bidirectional editing: selecting an existing text element on
|
||||
// the canvas (or via the layers panel, which is what we do here
|
||||
// since we don't want to deal with canvas pixel coordinates)
|
||||
// should put the Text tab into edit mode against that element.
|
||||
// Typing into the textarea then flows back to update the element's
|
||||
// text on canvas AND in the layers panel.
|
||||
await addTextViaSidebar(page, { text: 'Original' });
|
||||
|
||||
// Select the layer's row to make it the active element.
|
||||
await page.locator('.layers-item-main', { hasText: 'Original' }).click();
|
||||
|
||||
// The Text tab auto-switches on selection (desktop auto-tab-switch
|
||||
// effect in App.jsx). The textarea should now contain the
|
||||
// selected element's text.
|
||||
const textarea = page.getByLabel('Your message');
|
||||
await expect(textarea).toHaveValue('Original');
|
||||
|
||||
// Edit it. fill() clears + types, mirroring a user selecting all
|
||||
// and re-typing.
|
||||
await textarea.fill('Updated');
|
||||
|
||||
// The layer name in the panel should reflect the new text. The
|
||||
// update is debounced (300ms in useDesignEditor.updateElement),
|
||||
// and the layer name reads from element.text directly, so it
|
||||
// shows the new value as soon as state propagates.
|
||||
await expect(page.locator('.layers-item-name', { hasText: 'Updated' })).toBeVisible();
|
||||
|
||||
// And the count is still 1 — editing doesn't add a new element.
|
||||
expect(await getElementsCount(page)).toBe(1);
|
||||
});
|
||||
|
||||
test('deleting a text element from the Layers panel removes it', async ({ page }) => {
|
||||
await addTextViaSidebar(page, { text: 'Doomed' });
|
||||
await addTextViaSidebar(page, { text: 'Survivor' });
|
||||
|
||||
expect(await getElementsCount(page)).toBe(2);
|
||||
|
||||
// Each row has a trash-can delete button as a sibling of the
|
||||
// main row button. LayersPanel labels it
|
||||
// `aria-label="Delete ${getName(element)}"`, so we can target
|
||||
// the specific row's delete button by accessible name.
|
||||
await page.getByRole('button', { name: 'Delete Doomed' }).click();
|
||||
|
||||
expect(await getElementsCount(page)).toBe(1);
|
||||
// The remaining row is the one we didn't touch.
|
||||
await expect(page.locator('.layers-item-name', { hasText: 'Survivor' })).toBeVisible();
|
||||
// And the deleted row is gone.
|
||||
await expect(page.locator('.layers-item-name', { hasText: 'Doomed' })).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user