Files
odysseus/tests/e2e/photo-editor/group-reorder.spec.js
T

108 lines
5.4 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const { editorState, openBlankEditor, reopenDraft, waitForDraft } = require('./helpers.js');
test('group drag reorders the complete subtree with undo and server persistence', async ({ page, request }) => {
await openBlankEditor(page, { width: 420, height: 300 }, 'Group reorder E2E');
await page.locator('.ge-layer-item[data-layer-id]').filter({ hasText: 'Background' }).first().click({ modifiers: ['Control'] });
await page.locator('#ge-group-selected').click();
let current = await editorState(page);
const group = current.groups[0];
const originalOrder = current.layers.map(layer => layer.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;
expect(current.layers.map(layer => layer.id)).toEqual([...originalOrder, looseId]);
const handle = page.locator(`.ge-layer-group-row[data-group-id="${group.id}"] .ge-layer-group-drag`);
const looseRow = page.locator(`.ge-layer-item[data-layer-id="${looseId}"]`);
const handleBox = await handle.boundingBox();
const looseBox = await looseRow.boundingBox();
const dragReadiness = await page.evaluate(async ({ groupId, point }) => {
const { state } = await import('/static/js/editor/state.js');
const { groupSiblingUnits } = await import('/static/js/editor/layer-groups.js');
const group = state.layerGroups.find(item => item.id === groupId);
const hit = document.elementFromPoint(point.x, point.y);
return {
siblingIds: groupSiblingUnits(state, group.parentId || null).map(unit => unit.id),
hitHandle: !!hit?.closest('.ge-layer-group-drag'),
};
}, { groupId: group.id, point: { x: handleBox.x + handleBox.width / 2, y: handleBox.y + handleBox.height / 2 } });
expect(dragReadiness.hitHandle, JSON.stringify(dragReadiness)).toBe(true);
expect(dragReadiness.siblingIds).toEqual([group.id, looseId]);
await page.mouse.move(handleBox.x + handleBox.width / 2, handleBox.y + handleBox.height / 2);
await page.mouse.down();
await page.mouse.move(looseBox.x + looseBox.width / 2, looseBox.y + 1, { steps: 8 });
await expect(page.locator('.ge-group-drop-line')).toBeVisible();
await page.mouse.up();
current = await editorState(page);
expect(current.layers.map(layer => layer.id)).toEqual([looseId, ...originalOrder]);
expect(current.groups[0].layerIds).toEqual(group.layerIds);
await page.locator('#ge-undo').click();
expect((await editorState(page)).layers.map(layer => layer.id)).toEqual([...originalOrder, looseId]);
await page.locator('#ge-redo').click();
expect((await editorState(page)).layers.map(layer => layer.id)).toEqual([looseId, ...originalOrder]);
const draftId = await waitForDraft(page);
const beforeReopen = await editorState(page);
await reopenDraft(page, draftId);
current = await editorState(page);
expect(current.layers.map(layer => layer.id)).toEqual(beforeReopen.layers.map(layer => layer.id));
expect(current.groups).toEqual(beforeReopen.groups);
await request.delete(`/api/editor-drafts/${encodeURIComponent(draftId)}`);
});
test('mobile layer drag handle supports long-press reorder', async ({ browser, browserName }) => {
test.skip(browserName !== 'chromium', 'Uses Chromium CDP touch injection');
const context = await browser.newContext({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
serviceWorkers: 'block',
});
const page = await context.newPage();
try {
await openBlankEditor(page, { width: 320, height: 240 }, 'Mobile layer reorder');
await page.locator('.ge-layers-header').click();
await expect(page.locator('.ge-right-panel')).toHaveClass(/expanded/);
const before = await editorState(page);
// State is bottom-to-top; the panel is top-to-bottom.
const dragId = before.layers[0].id;
const targetId = before.layers[before.layers.length - 1].id;
const bottomHandle = page.locator(`.ge-layer-item[data-layer-id="${dragId}"] .ge-layer-drag`);
const topRow = page.locator(`.ge-layer-item[data-layer-id="${targetId}"]`);
await bottomHandle.scrollIntoViewIfNeeded();
const handleBox = await bottomHandle.boundingBox();
const topBox = await topRow.boundingBox();
expect(handleBox.width).toBeGreaterThanOrEqual(24);
const point = { x: handleBox.x + handleBox.width / 2, y: handleBox.y + handleBox.height / 2 };
const hitHandle = await page.evaluate(({ x, y }) => {
const hit = document.elementFromPoint(x, y);
return !!hit?.closest('.ge-layer-drag');
}, point);
expect(hitHandle).toBe(true);
const cdp = await page.context().newCDPSession(page);
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x: point.x, y: point.y, id: 1, radiusX: 8, radiusY: 8 }],
});
await page.waitForTimeout(450);
await expect(page.locator(`.ge-layer-item[data-layer-id="${dragId}"]`)).toHaveClass(/dragging/);
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: point.x, y: topBox.y + 1, id: 1, radiusX: 8, radiusY: 8 }],
});
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
});
await page.waitForTimeout(120);
expect((await editorState(page)).layers.map(layer => layer.id)).toEqual([targetId, dragId]);
} finally {
await context.close();
}
});