mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-25 17:42:20 +02:00
305 lines
16 KiB
JavaScript
305 lines
16 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { dragOnCanvas, editorState, openBlankEditor, reopenDraft, waitForDraft } = require('./helpers.js');
|
|
|
|
test('selected layers align to the canvas and undo as one operation', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 320, height: 240 }, 'Alignment E2E');
|
|
await page.locator('#ge-add-layer').click();
|
|
await page.locator('.ge-add-layer-menu [data-layer-kind="raster"]').click();
|
|
await page.evaluate(async () => {
|
|
const { state } = await import('/static/js/editor/state.js');
|
|
const edit = state.layers.find(layer => layer.name === 'Edit');
|
|
const added = state.layers.find(layer => layer.id === state.activeLayerId);
|
|
edit.canvas.width = 100;
|
|
edit.canvas.height = 80;
|
|
added.canvas.width = 50;
|
|
added.canvas.height = 40;
|
|
state.layerOffsets.set(edit.id, { x: 12, y: 18 });
|
|
state.layerOffsets.set(added.id, { x: 190, y: 150 });
|
|
});
|
|
const editRow = page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Edit' }).first();
|
|
await editRow.click({ modifiers: ['Control'] });
|
|
await expect(page.locator('.ge-layer-item.selected[data-layer-id]')).toHaveCount(2);
|
|
await page.locator('#ge-selected-align').click();
|
|
await page.locator('#ge-layer-align-menu button').filter({ hasText: 'Align center' }).click();
|
|
|
|
let current = await editorState(page);
|
|
expect(current.layers.find(layer => layer.name === 'Edit').offset).toEqual({ x: 110, y: 18 });
|
|
expect(current.layers.find(layer => layer.name !== 'Background' && layer.name !== 'Edit').offset)
|
|
.toEqual({ x: 135, y: 150 });
|
|
|
|
await page.locator('#ge-undo').click();
|
|
current = await editorState(page);
|
|
expect(current.layers.find(layer => layer.name === 'Edit').offset).toEqual({ x: 12, y: 18 });
|
|
expect(current.layers.find(layer => layer.name !== 'Background' && layer.name !== 'Edit').offset)
|
|
.toEqual({ x: 190, y: 150 });
|
|
});
|
|
|
|
test('layer rename cancels on Escape without committing the draft name', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 240, height: 160 }, 'Rename cancel E2E');
|
|
const row = page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Edit' }).first();
|
|
const rowId = await row.getAttribute('data-layer-id');
|
|
const name = row.locator('.ge-layer-name');
|
|
const original = await name.textContent();
|
|
await name.dblclick();
|
|
const stableRow = page.locator(`.ge-layer-item[data-layer-id="${rowId}"]`);
|
|
const input = stableRow.locator('.ge-layer-name-input');
|
|
await input.fill('Temporary name');
|
|
await input.press('Escape');
|
|
await expect(stableRow.locator('.ge-layer-name')).toHaveText(original);
|
|
await expect(stableRow.locator('.ge-layer-name-input')).toHaveCount(0);
|
|
});
|
|
|
|
test('layer rows can be selected with Enter and Space', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 240, height: 160 }, 'Keyboard layer selection E2E');
|
|
const background = page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Background' }).first();
|
|
const edit = page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Edit' }).first();
|
|
|
|
await background.focus();
|
|
await page.keyboard.press('Enter');
|
|
await expect(background).toHaveAttribute('aria-pressed', 'true');
|
|
await expect(edit).toHaveAttribute('aria-pressed', 'false');
|
|
|
|
await edit.focus();
|
|
await page.keyboard.press('Space');
|
|
await expect(edit).toHaveAttribute('aria-pressed', 'true');
|
|
await expect(background).toHaveAttribute('aria-pressed', 'false');
|
|
});
|
|
|
|
test('group rows can be selected with the keyboard', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 240, height: 160 }, 'Keyboard group selection E2E');
|
|
await page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Background' }).first().click({ modifiers: ['Control'] });
|
|
await page.locator('#ge-group-selected').click();
|
|
|
|
const group = page.locator('.ge-layer-group-row').first();
|
|
await group.focus();
|
|
await page.keyboard.press('Enter');
|
|
await expect(group).toHaveAttribute('aria-pressed', 'true');
|
|
await expect.poll(async () => page.evaluate(async () => {
|
|
const { state } = await import('/static/js/editor/state.js');
|
|
return state.activeGroupId;
|
|
})).toBe(await group.getAttribute('data-group-id'));
|
|
});
|
|
|
|
test('Delete removes the selected layer and undo restores it', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 240, height: 160 }, 'Keyboard delete E2E');
|
|
await page.locator('#ge-add-layer').click();
|
|
await page.locator('.ge-add-layer-menu [data-layer-kind="raster"]').click();
|
|
const addedId = (await editorState(page)).activeLayerId;
|
|
await page.locator(`.ge-layer-item[data-layer-id="${addedId}"]`).click();
|
|
await page.keyboard.press('Backspace');
|
|
expect((await editorState(page)).layers.some(layer => layer.id === addedId)).toBe(false);
|
|
|
|
await page.locator('#ge-undo').click();
|
|
expect((await editorState(page)).layers.some(layer => layer.id === addedId)).toBe(true);
|
|
});
|
|
|
|
test('Ctrl/Cmd+J duplicates the active layer through the layer panel path', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 240, height: 160 }, 'Keyboard duplicate E2E');
|
|
const before = await editorState(page);
|
|
const activeId = before.activeLayerId;
|
|
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+j' : 'Control+j');
|
|
const after = await editorState(page);
|
|
expect(after.layers).toHaveLength(before.layers.length + 1);
|
|
expect(after.activeLayerId).not.toBe(activeId);
|
|
expect(after.layers.find(layer => layer.id === after.activeLayerId).name).toContain('copy');
|
|
});
|
|
|
|
test('move tool can auto-select the topmost visible layer under the pointer', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 320, height: 240 }, 'Auto-select E2E');
|
|
await page.locator('.ge-tool-btn[data-tool="shape"]').click();
|
|
await dragOnCanvas(page, { x: 0.25, y: 0.25 }, { x: 0.75, y: 0.75 });
|
|
const shape = (await editorState(page)).layers.find(layer => layer.kind === 'shape');
|
|
expect(shape).toBeTruthy();
|
|
|
|
await page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Background' }).first().click();
|
|
await page.locator('.ge-tool-btn[data-tool="move"]').click();
|
|
await expect(page.locator('.ge-auto-select-option')).toBeVisible();
|
|
await page.locator('.ge-auto-select-option').click();
|
|
await expect(page.locator('#ge-auto-select-layer')).toBeChecked();
|
|
await page.locator('.ge-tool-btn[data-tool="transform"]').click();
|
|
await expect(page.locator('.ge-auto-select-option')).toBeHidden();
|
|
await page.locator('.ge-tool-btn[data-tool="move"]').click();
|
|
await expect(page.locator('.ge-auto-select-option')).toBeVisible();
|
|
const box = await page.locator('.ge-main-canvas').boundingBox();
|
|
await page.mouse.click(box.x + box.width * 0.5, box.y + box.height * 0.5);
|
|
await expect.poll(async () => (await editorState(page)).activeLayerId).toBe(shape.id);
|
|
expect((await editorState(page)).selectedLayerIds).toEqual([shape.id]);
|
|
});
|
|
|
|
test('multi-selected layers move together and support history-backed bulk actions', async ({ page }) => {
|
|
await openBlankEditor(page);
|
|
const editRow = page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Edit' }).first();
|
|
const backgroundRow = page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Background' }).first();
|
|
|
|
await backgroundRow.click({ modifiers: ['Control'] });
|
|
await expect(page.locator('.ge-layer-item.selected[data-layer-id]')).toHaveCount(2);
|
|
expect((await editorState(page)).selectedLayerIds).toHaveLength(2);
|
|
await expect(page.locator('#ge-layer-selection-bar')).toBeVisible();
|
|
|
|
await page.locator('.ge-tool-btn[data-tool="move"]').click();
|
|
await dragOnCanvas(page, { x: 0.42, y: 0.42 }, { x: 0.52, y: 0.50 });
|
|
let current = await editorState(page);
|
|
const edit = current.layers.find(layer => layer.name === 'Edit');
|
|
const background = current.layers.find(layer => layer.name === 'Background');
|
|
expect(edit.offset).toEqual(background.offset);
|
|
expect(edit.offset).not.toEqual({ x: 0, y: 0 });
|
|
|
|
await page.locator('#ge-undo').click();
|
|
current = await editorState(page);
|
|
expect(current.layers.find(layer => layer.name === 'Edit').offset).toEqual({ x: 0, y: 0 });
|
|
expect(current.layers.find(layer => layer.name === 'Background').offset).toEqual({ x: 0, y: 0 });
|
|
expect(current.selectedLayerIds).toHaveLength(2);
|
|
await page.locator('#ge-redo').click();
|
|
|
|
await page.locator('#ge-selected-visibility').click();
|
|
current = await editorState(page);
|
|
expect(current.layers.every(layer => layer.visible === false)).toBe(true);
|
|
await page.locator('#ge-undo').click();
|
|
current = await editorState(page);
|
|
expect(current.layers.every(layer => layer.visible !== false)).toBe(true);
|
|
|
|
await page.locator('#ge-selected-lock').click();
|
|
current = await editorState(page);
|
|
expect(current.layers.every(layer => layer.locked)).toBe(true);
|
|
await page.locator('#ge-undo').click();
|
|
|
|
await editRow.click();
|
|
await page.locator('#ge-add-layer').click();
|
|
await page.locator('.ge-add-layer-menu [data-layer-kind="raster"]').click();
|
|
const addedId = (await editorState(page)).activeLayerId;
|
|
await editRow.click({ modifiers: ['Control'] });
|
|
await expect(page.locator('.ge-layer-item.selected[data-layer-id]')).toHaveCount(2);
|
|
await page.locator('#ge-selected-delete').click();
|
|
current = await editorState(page);
|
|
expect(current.layers).toHaveLength(1);
|
|
expect(current.layers[0].name).toBe('Background');
|
|
expect(current.layers.some(layer => layer.id === addedId)).toBe(false);
|
|
|
|
await page.locator('#ge-undo').click();
|
|
current = await editorState(page);
|
|
expect(current.layers).toHaveLength(3);
|
|
expect(current.selectedLayerIds).toHaveLength(2);
|
|
});
|
|
|
|
test('layer groups composite, lock, collapse, undo, and survive server reopen', async ({ page, request }) => {
|
|
await openBlankEditor(page, { width: 480, height: 320 }, 'Grouped E2E');
|
|
const backgroundRow = page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Background' }).first();
|
|
await backgroundRow.click({ modifiers: ['Control'] });
|
|
await page.locator('#ge-group-selected').click();
|
|
await expect(page.locator('.ge-layer-group-row')).toHaveCount(1);
|
|
await expect(page.locator('.ge-layer-group-row .ge-group-inline-thumb')).toHaveCount(1);
|
|
let current = await editorState(page);
|
|
expect(current.groups).toHaveLength(1);
|
|
expect(current.groups[0].layerIds).toHaveLength(2);
|
|
|
|
const groupName = page.locator('.ge-layer-group-name');
|
|
await groupName.dblclick();
|
|
await page.locator('.ge-layer-group-row input.ge-layer-name-input').fill('Hero group');
|
|
await page.locator('.ge-layer-group-row input.ge-layer-name-input').press('Enter');
|
|
await page.locator('.ge-layer-group-row .ge-layer-opacity').fill('55');
|
|
await page.locator('.ge-layer-group-toggle').click();
|
|
await expect(page.locator('.ge-layer-item.grouped')).toHaveCount(0);
|
|
current = await editorState(page);
|
|
expect(current.groups[0]).toMatchObject({ name: 'Hero group', opacity: 0.55, collapsed: true });
|
|
|
|
await page.locator('.ge-layer-group-row .ge-layer-lock-btn').click();
|
|
const beforeLockNudge = await editorState(page);
|
|
await page.keyboard.press('ArrowRight');
|
|
expect((await editorState(page)).layers.map(layer => layer.offset)).toEqual(beforeLockNudge.layers.map(layer => layer.offset));
|
|
await page.locator('.ge-layer-group-row .ge-layer-lock-btn').click();
|
|
|
|
await page.locator('.ge-layer-group-row').click();
|
|
await page.locator('.ge-tool-btn[data-tool="move"]').click();
|
|
await dragOnCanvas(page, { x: 0.40, y: 0.40 }, { x: 0.52, y: 0.48 });
|
|
current = await editorState(page);
|
|
expect(current.layers[0].offset).toEqual(current.layers[1].offset);
|
|
expect(current.layers[0].offset).not.toEqual({ x: 0, y: 0 });
|
|
|
|
await page.locator('.ge-layer-group-row .ge-layer-vis').click();
|
|
current = await editorState(page);
|
|
expect(current.groups[0].visible).toBe(false);
|
|
expect(current.layers.every(layer => layer.visible)).toBe(true);
|
|
await page.locator('#ge-undo').click();
|
|
expect((await editorState(page)).groups[0].visible).toBe(true);
|
|
|
|
const draftId = await waitForDraft(page);
|
|
const beforeReopen = await editorState(page);
|
|
await reopenDraft(page, draftId);
|
|
const reopened = await editorState(page);
|
|
expect(reopened.groups).toEqual(beforeReopen.groups);
|
|
expect(reopened.layers.map(layer => layer.offset)).toEqual(beforeReopen.layers.map(layer => layer.offset));
|
|
await expect(page.locator('.ge-layer-group-row')).toHaveCount(1);
|
|
await expect(page.locator('.ge-layer-group-row .ge-group-inline-thumb')).toHaveCount(1);
|
|
await expect(page.locator('.ge-layer-item.grouped')).toHaveCount(0);
|
|
|
|
await page.locator('.ge-layer-group-row').click();
|
|
await page.locator('#ge-layer-tools button[title="Ungroup layers"]').click();
|
|
expect((await editorState(page)).groups).toHaveLength(0);
|
|
await page.locator('#ge-undo').click();
|
|
expect((await editorState(page)).groups).toHaveLength(1);
|
|
|
|
await request.delete(`/api/editor-drafts/${encodeURIComponent(draftId)}`);
|
|
});
|
|
|
|
test('nested groups preserve hierarchy, ancestor locks, collapse, and server reopen', async ({ page, request }) => {
|
|
await openBlankEditor(page, { width: 420, height: 300 }, 'Nested groups E2E');
|
|
const backgroundRow = page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Background' }).first();
|
|
await backgroundRow.click({ modifiers: ['Control'] });
|
|
await page.locator('#ge-group-selected').click();
|
|
let current = await editorState(page);
|
|
const innerId = current.groups[0].id;
|
|
|
|
await page.locator('#ge-add-layer').click();
|
|
await page.locator('.ge-add-layer-menu [data-layer-kind="raster"]').click();
|
|
current = await editorState(page);
|
|
const looseId = current.activeLayerId;
|
|
await page.locator(`.ge-layer-group-row[data-group-id="${innerId}"]`).click();
|
|
await page.locator(`.ge-layer-item[data-layer-id="${looseId}"]`).click({ modifiers: ['Control'] });
|
|
await expect(page.locator('.ge-layer-item.selected[data-layer-id]')).toHaveCount(3);
|
|
await page.locator('#ge-group-selected').click();
|
|
|
|
current = await editorState(page);
|
|
expect(current.groups).toHaveLength(2);
|
|
const inner = current.groups.find(group => group.id === innerId);
|
|
const outer = current.groups.find(group => group.id !== innerId);
|
|
expect(inner.parentId).toBe(outer.id);
|
|
expect(outer.layerIds).toEqual([looseId]);
|
|
expect(inner.layerIds).toHaveLength(2);
|
|
await expect(page.locator('.ge-layer-group-row')).toHaveCount(2);
|
|
await expect(page.locator('.ge-layer-group-row .ge-group-inline-thumb')).toHaveCount(2);
|
|
await expect(page.locator(`.ge-layer-group-row[data-group-id="${inner.id}"]`)).toHaveCSS('--group-depth', '1');
|
|
|
|
const outerRow = page.locator(`.ge-layer-group-row[data-group-id="${outer.id}"]`);
|
|
await outerRow.locator('.ge-layer-group-toggle').click();
|
|
await expect(page.locator('.ge-layer-group-row')).toHaveCount(1);
|
|
await expect(page.locator('.ge-layer-item.grouped')).toHaveCount(0);
|
|
await outerRow.locator('.ge-layer-group-toggle').click();
|
|
await expect(page.locator('.ge-layer-group-row')).toHaveCount(2);
|
|
|
|
await outerRow.locator('.ge-layer-lock-btn').click();
|
|
await page.locator(`.ge-layer-item[data-layer-id="${inner.layerIds[0]}"]`).click();
|
|
const beforeNudge = await editorState(page);
|
|
await page.keyboard.press('ArrowRight');
|
|
expect((await editorState(page)).layers.map(layer => layer.offset)).toEqual(beforeNudge.layers.map(layer => layer.offset));
|
|
await outerRow.locator('.ge-layer-lock-btn').click();
|
|
|
|
const draftId = await waitForDraft(page);
|
|
const beforeReopen = await editorState(page);
|
|
await reopenDraft(page, draftId);
|
|
current = await editorState(page);
|
|
expect(current.groups).toEqual(beforeReopen.groups);
|
|
await expect(page.locator('.ge-layer-group-row')).toHaveCount(2);
|
|
|
|
await page.locator(`.ge-layer-group-row[data-group-id="${outer.id}"]`).click();
|
|
await page.locator('#ge-layer-tools button[title="Ungroup layers"]').click();
|
|
current = await editorState(page);
|
|
expect(current.groups).toHaveLength(1);
|
|
expect(current.groups[0]).toMatchObject({ id: inner.id, parentId: null });
|
|
expect(current.groups[0].layerIds).toHaveLength(2);
|
|
await expect(page.locator('.ge-layer-item.grouped')).toHaveCount(2);
|
|
await page.locator('#ge-undo').click();
|
|
expect((await editorState(page)).groups).toEqual(beforeReopen.groups);
|
|
|
|
await request.delete(`/api/editor-drafts/${encodeURIComponent(draftId)}`);
|
|
});
|