mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-07 23:42:21 +02:00
335 lines
16 KiB
JavaScript
335 lines
16 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { dragOnCanvas, editorState, openBlankEditor, reopenDraft, waitForDraft } = require('./helpers.js');
|
|
|
|
test('text edits directly on canvas and remains retained after reopen', async ({ page }) => {
|
|
await openBlankEditor(page);
|
|
await page.locator('.ge-tool-btn[data-tool="text"]').click();
|
|
await page.locator('#ge-text-frame-width').fill('260');
|
|
const canvas = await page.locator('.ge-main-canvas').boundingBox();
|
|
await page.mouse.click(canvas.x + canvas.width * 0.2, canvas.y + canvas.height * 0.2);
|
|
const editor = page.locator('.ge-direct-text-editor');
|
|
await expect(editor).toBeVisible();
|
|
await editor.fill('Editable canvas title');
|
|
await editor.press('Control+Enter');
|
|
await expect(editor).toHaveCount(0);
|
|
|
|
await page.locator('#ge-text-letter-spacing').fill('3.5');
|
|
await page.locator('#ge-text-line-height').fill('1.4');
|
|
await page.locator('#ge-text-font').selectOption('Georgia');
|
|
await page.locator('.ge-text-auto-width-option').click();
|
|
await expect(page.locator('#ge-text-auto-width')).toBeChecked();
|
|
await expect(page.locator('#ge-text-frame-width')).toBeDisabled();
|
|
await page.locator('#ge-text-frame-height').fill('180');
|
|
await page.locator('#ge-text-vertical-align').selectOption('bottom');
|
|
const before = await editorState(page);
|
|
const textLayer = before.layers.find(layer => layer.kind === 'text');
|
|
expect(textLayer.text).toMatchObject({
|
|
content: 'Editable canvas title',
|
|
fontFamily: 'Georgia',
|
|
lineHeight: 1.4,
|
|
letterSpacing: 3.5,
|
|
autoWidth: true,
|
|
frameHeight: 180,
|
|
verticalAlign: 'bottom',
|
|
});
|
|
|
|
const draftId = await waitForDraft(page);
|
|
await reopenDraft(page, draftId);
|
|
const reopened = await editorState(page);
|
|
expect(reopened.layers.find(layer => layer.kind === 'text').text).toEqual(textLayer.text);
|
|
});
|
|
|
|
test('text paragraph controls remain usable in a narrow phone viewport', async ({ page }) => {
|
|
await page.setViewportSize({ width: 320, height: 700 });
|
|
await openBlankEditor(page, { width: 240, height: 180 }, 'Mobile text layout E2E');
|
|
await page.locator('.ge-tool-btn[data-tool="text"]').click();
|
|
const section = page.locator('#ge-text-section');
|
|
await expect(section).toBeVisible();
|
|
expect(await section.evaluate(el => el.scrollWidth <= el.clientWidth + 1)).toBe(true);
|
|
await expect(page.locator('#ge-text-frame-height')).toBeVisible();
|
|
await expect(page.locator('#ge-text-vertical-align')).toBeVisible();
|
|
});
|
|
|
|
test('rectangle ellipse line and polygon remain editable shape layers', async ({ page }) => {
|
|
await openBlankEditor(page);
|
|
await page.locator('.ge-tool-btn[data-tool="shape"]').click();
|
|
await expect(page.locator('#ge-shape-section')).toBeVisible();
|
|
|
|
const types = ['rectangle', 'ellipse', 'line', 'polygon'];
|
|
for (let index = 0; index < types.length; index += 1) {
|
|
const type = types[index];
|
|
await page.locator('.ge-layer-item').filter({ hasText: 'Edit' }).first().click();
|
|
await page.locator(`[data-shape-type="${type}"]`).click();
|
|
await dragOnCanvas(
|
|
page,
|
|
{ x: 0.12 + index * 0.18, y: 0.2 },
|
|
{ x: 0.25 + index * 0.18, y: 0.42 },
|
|
);
|
|
}
|
|
|
|
let current = await editorState(page);
|
|
const shapes = current.layers.filter(layer => layer.kind === 'shape');
|
|
expect(shapes.map(layer => layer.shape.type)).toEqual(types);
|
|
await page.locator('#ge-shape-sides').fill('7');
|
|
await page.locator('#ge-shape-stroke-width').fill('6');
|
|
await page.locator('#ge-shape-radius').fill('14');
|
|
current = await editorState(page);
|
|
const polygon = current.layers.find(layer => layer.kind === 'shape' && layer.shape.type === 'polygon');
|
|
expect(polygon.shape).toMatchObject({ sides: 7, strokeWidth: 6, cornerRadius: 14 });
|
|
|
|
await page.locator('.ge-tool-btn[data-tool="transform"]').click();
|
|
const originalWidth = Number(await page.locator('#ge-transform-w').inputValue());
|
|
await page.locator('#ge-transform-w').fill(String(originalWidth + 40));
|
|
await page.locator('#ge-transform-apply').click();
|
|
current = await editorState(page);
|
|
const transformed = current.layers.find(layer => layer.id === polygon.id);
|
|
expect(transformed.kind).toBe('shape');
|
|
expect(transformed.shape.transform.scaleX).toBeGreaterThan(1);
|
|
|
|
const draftId = await waitForDraft(page);
|
|
await reopenDraft(page, draftId);
|
|
current = await editorState(page);
|
|
expect(current.layers.filter(layer => layer.kind === 'shape').map(layer => layer.shape.type)).toEqual(types);
|
|
});
|
|
|
|
test('gradient shape fill remains editable after reopening the project', async ({ page }) => {
|
|
await openBlankEditor(page);
|
|
await page.locator('.ge-tool-btn[data-tool="shape"]').click();
|
|
await expect(page.locator('#ge-shape-gradient-add-stop')).toBeHidden();
|
|
await dragOnCanvas(page, { x: 0.2, y: 0.2 }, { x: 0.65, y: 0.55 });
|
|
await page.locator('#ge-shape-gradient-angle').evaluate((angle) => {
|
|
const section = angle.closest('#ge-shape-section');
|
|
section.querySelector('#ge-shape-fill-type').value = 'linear-gradient';
|
|
section.querySelector('#ge-shape-gradient-start').value = '#ff0000';
|
|
section.querySelector('#ge-shape-gradient-mid').value = '#00ff00';
|
|
section.querySelector('#ge-shape-gradient-mid-enabled').checked = true;
|
|
section.querySelector('#ge-shape-gradient-mid-position').value = '42';
|
|
section.querySelector('#ge-shape-gradient-end').value = '#0000ff';
|
|
section.querySelector('#ge-shape-fill-type').dispatchEvent(new Event('change', { bubbles: true }));
|
|
angle.value = '35';
|
|
angle.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
await expect(page.locator('#ge-shape-gradient-add-stop')).toBeVisible();
|
|
|
|
const before = await editorState(page);
|
|
const shape = before.layers.find(layer => layer.kind === 'shape');
|
|
expect(shape.shape).toMatchObject({
|
|
fillType: 'linear-gradient',
|
|
gradientStart: '#ff0000',
|
|
gradientMid: '#00ff00',
|
|
gradientMidEnabled: true,
|
|
gradientMidPosition: 42,
|
|
gradientEnd: '#0000ff',
|
|
gradientAngle: 35,
|
|
});
|
|
expect(page.locator('.ge-layer-inline-thumb')).toHaveCount(before.layers.length);
|
|
const thumbnailColorRange = await page.locator('.ge-layer-inline-thumb').evaluateAll((canvases) => canvases.map(canvas => {
|
|
const pixels = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
|
|
return {
|
|
red: Math.max(...Array.from(pixels).filter((_, index) => index % 4 === 0)),
|
|
blue: Math.max(...Array.from(pixels).filter((_, index) => index % 4 === 2)),
|
|
};
|
|
}));
|
|
expect(thumbnailColorRange.some(({ red, blue }) => red > 180 && blue > 180)).toBe(true);
|
|
|
|
const draftId = await waitForDraft(page);
|
|
await reopenDraft(page, draftId);
|
|
const reopened = await editorState(page);
|
|
expect(reopened.layers.find(layer => layer.id === shape.id).shape).toMatchObject(shape.shape);
|
|
});
|
|
|
|
test('shape gradients retain added stops through editing and reopen', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 360, height: 240 }, 'Multi-stop gradient E2E');
|
|
await page.locator('.ge-tool-btn[data-tool="shape"]').click();
|
|
await dragOnCanvas(page, { x: 0.15, y: 0.2 }, { x: 0.75, y: 0.65 });
|
|
await page.locator('#ge-shape-fill-type').selectOption('linear-gradient');
|
|
await page.evaluate(() => {
|
|
for (const [id, value] of [['ge-shape-gradient-start', '#ff0000'], ['ge-shape-gradient-end', '#0000ff']]) {
|
|
const input = document.getElementById(id);
|
|
input.value = value;
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
});
|
|
await page.locator('#ge-shape-gradient-add-stop').click();
|
|
await expect(page.locator('[data-gradient-extra-stop]')).toHaveCount(1);
|
|
await page.evaluate(() => {
|
|
const color = document.querySelector('[data-gradient-stop-color]');
|
|
color.value = '#00ff00';
|
|
color.dispatchEvent(new Event('input', { bubbles: true }));
|
|
const position = document.querySelector('[data-gradient-stop-position]');
|
|
position.value = '50';
|
|
position.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
await page.locator('#ge-shape-gradient-add-stop').click();
|
|
await expect(page.locator('[data-gradient-extra-stop]')).toHaveCount(2);
|
|
await page.evaluate(() => {
|
|
const rows = [...document.querySelectorAll('[data-gradient-extra-stop]')];
|
|
const setRow = (row, colorValue, positionValue) => {
|
|
const color = row.querySelector('[data-gradient-stop-color]');
|
|
color.value = colorValue;
|
|
color.dispatchEvent(new Event('input', { bubbles: true }));
|
|
const position = row.querySelector('[data-gradient-stop-position]');
|
|
position.value = positionValue;
|
|
position.dispatchEvent(new Event('input', { bubbles: true }));
|
|
};
|
|
setRow(rows[0], '#00ff00', '50');
|
|
setRow(rows[1], '#ffff00', '75');
|
|
});
|
|
|
|
let current = await editorState(page);
|
|
const shape = current.layers.find(layer => layer.kind === 'shape');
|
|
expect(shape.shape.gradientStops).toEqual([
|
|
{ position: 0, color: '#ff0000' },
|
|
{ position: 50, color: '#00ff00' },
|
|
{ position: 75, color: '#ffff00' },
|
|
{ position: 100, color: '#0000ff' },
|
|
]);
|
|
|
|
await page.locator('[data-gradient-extra-stop]').first().locator('[data-gradient-stop-remove]').click();
|
|
current = await editorState(page);
|
|
expect(current.layers.find(layer => layer.id === shape.id).shape.gradientStops).toEqual([
|
|
{ position: 0, color: '#ff0000' },
|
|
{ position: 75, color: '#ffff00' },
|
|
{ position: 100, color: '#0000ff' },
|
|
]);
|
|
|
|
const draftId = await waitForDraft(page);
|
|
await reopenDraft(page, draftId);
|
|
const reopened = await editorState(page);
|
|
expect(reopened.layers.find(layer => layer.id === shape.id).shape.gradientStops).toEqual([
|
|
{ position: 0, color: '#ff0000' },
|
|
{ position: 75, color: '#ffff00' },
|
|
{ position: 100, color: '#0000ff' },
|
|
]);
|
|
});
|
|
|
|
test('shape gradient stop normalization keeps imported endpoints bounded', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 240, height: 180 }, 'Gradient stop bounds E2E');
|
|
const result = await page.evaluate(async () => {
|
|
const { MAX_GRADIENT_STOPS, normalizeGradientStops } = await import('/static/js/editor/gradient-stops.js');
|
|
const stops = normalizeGradientStops(Array.from({ length: 30 }, (_, index) => ({
|
|
position: index * 3,
|
|
color: `#${String(index).padStart(6, '0')}`,
|
|
})));
|
|
return { max: MAX_GRADIENT_STOPS, stops };
|
|
});
|
|
expect(result.stops).toHaveLength(result.max);
|
|
expect(result.stops[0].position).toBe(0);
|
|
expect(result.stops.at(-1).position).toBe(100);
|
|
});
|
|
|
|
test('gradient tool paints a reversible drag on the active layer', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 320, height: 240 }, 'Gradient tool E2E');
|
|
await page.locator('.ge-tool-btn[data-tool="gradient"]').click();
|
|
await expect(page.locator('#ge-gradient-section')).toBeVisible();
|
|
|
|
await page.evaluate(() => {
|
|
for (const [id, value] of [['ge-gradient-start', '#ff0000'], ['ge-gradient-end', '#0000ff']]) {
|
|
const input = document.getElementById(id);
|
|
input.value = value;
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
const midpoint = document.getElementById('ge-gradient-mid');
|
|
midpoint.value = '#00ff00';
|
|
midpoint.dispatchEvent(new Event('input', { bubbles: true }));
|
|
const enabled = document.getElementById('ge-gradient-mid-enabled');
|
|
enabled.checked = true;
|
|
enabled.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
await page.locator('#ge-gradient-add-stop').click();
|
|
await page.evaluate(() => {
|
|
const row = document.querySelector('#ge-gradient-extra-stops [data-gradient-extra-stop]');
|
|
const color = row.querySelector('[data-gradient-stop-color]');
|
|
color.value = '#ffff00';
|
|
color.dispatchEvent(new Event('input', { bubbles: true }));
|
|
const position = row.querySelector('[data-gradient-stop-position]');
|
|
position.value = '25';
|
|
position.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
|
|
const box = await page.locator('.ge-main-canvas').boundingBox();
|
|
await page.mouse.move(box.x + 8, box.y + box.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(box.x + box.width - 8, box.y + box.height / 2, { steps: 8 });
|
|
await page.mouse.up();
|
|
|
|
await expect.poll(async () => (await editorState(page)).layers.at(-1).effects?.length || 0).toBe(1);
|
|
await expect.poll(async () => (await editorState(page)).documentRenderReady).toBe(true);
|
|
|
|
const samples = await page.evaluate(async () => {
|
|
const { state } = await import('/static/js/editor/state.js');
|
|
const layer = state.layers.find(item => item.id === state.activeLayerId);
|
|
const rendered = state.documentCompositeCanvas;
|
|
const ctx = rendered.getContext('2d');
|
|
const y = Math.floor(rendered.height / 2);
|
|
return [
|
|
ctx.getImageData(4, y, 1, 1).data,
|
|
ctx.getImageData(Math.floor(rendered.width / 2), y, 1, 1).data,
|
|
ctx.getImageData(rendered.width - 5, y, 1, 1).data,
|
|
];
|
|
});
|
|
expect(samples[0][0]).toBeGreaterThan(220);
|
|
expect(samples[0][2]).toBeLessThan(40);
|
|
expect(samples[1][1]).toBeGreaterThan(180);
|
|
expect(samples[1][0]).toBeLessThan(80);
|
|
expect(samples[1][2]).toBeLessThan(80);
|
|
expect(samples[2][2]).toBeGreaterThan(220);
|
|
expect(samples[2][0]).toBeLessThan(40);
|
|
|
|
const draftId = await waitForDraft(page);
|
|
await reopenDraft(page, draftId);
|
|
const reopened = await editorState(page);
|
|
const reopenedGradient = reopened.layers.find(layer => layer.effects?.some(effect => effect.type === 'linear-gradient'));
|
|
expect(reopenedGradient).toBeTruthy();
|
|
expect(reopenedGradient.effects.find(effect => effect.type === 'linear-gradient').params.stops).toEqual([
|
|
{ position: 0, color: '#ff0000', alpha: 1 },
|
|
{ position: 25, color: '#ffff00', alpha: 1 },
|
|
{ position: 50, color: '#00ff00', alpha: 1 },
|
|
{ position: 100, color: '#0000ff', alpha: 1 },
|
|
]);
|
|
|
|
await page.locator('.ge-effect-sub-item .ge-adj-sub-name').click();
|
|
await expect(page.locator('.ge-filter-modal')).toBeVisible();
|
|
await expect(page.locator('.ge-filter-row input[data-key="stopColor0"]')).toHaveValue('#ffff00');
|
|
await expect(page.locator('.ge-filter-row input[data-key="stopPosition0"]')).toHaveValue('25');
|
|
await expect(page.locator('.ge-filter-row input[data-key="stopColor1"]')).toHaveValue('#00ff00');
|
|
await page.locator('.ge-filter-row input[data-key="stopColor0"]').fill('#ffff00');
|
|
await page.locator('.ge-filter-row input[data-key="stopPosition0"]').fill('30');
|
|
await page.locator('.ge-filter-modal [data-action="apply"]').click();
|
|
const edited = (await editorState(page)).layers
|
|
.flatMap(layer => layer.effects || [])
|
|
.find(effect => effect.type === 'linear-gradient');
|
|
expect(edited.params.stops).toEqual([
|
|
{ position: 0, color: '#ff0000', alpha: 1 },
|
|
{ position: 30, color: '#ffff00', alpha: 1 },
|
|
{ position: 50, color: '#00ff00', alpha: 1 },
|
|
{ position: 100, color: '#0000ff', alpha: 1 },
|
|
]);
|
|
});
|
|
|
|
test('radial gradient remains retained and survives reopen', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 320, height: 240 }, 'Radial gradient E2E');
|
|
await page.locator('.ge-tool-btn[data-tool="gradient"]').click();
|
|
await page.locator('#ge-gradient-type').selectOption('radial-gradient');
|
|
await page.evaluate(() => {
|
|
const start = document.getElementById('ge-gradient-start');
|
|
start.value = '#ffffff';
|
|
start.dispatchEvent(new Event('input', { bubbles: true }));
|
|
const end = document.getElementById('ge-gradient-end');
|
|
end.value = '#000000';
|
|
end.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
await dragOnCanvas(page, { x: 0.5, y: 0.5 }, { x: 0.85, y: 0.5 });
|
|
let current = await editorState(page);
|
|
const gradient = current.layers.flatMap(layer => layer.effects || []).find(effect => effect.type === 'radial-gradient');
|
|
expect(gradient).toBeTruthy();
|
|
expect(gradient.params.stops).toEqual([
|
|
{ position: 0, color: '#ffffff', alpha: 1 },
|
|
{ position: 100, color: '#000000', alpha: 1 },
|
|
]);
|
|
const draftId = await waitForDraft(page);
|
|
await reopenDraft(page, draftId);
|
|
current = await editorState(page);
|
|
expect(current.layers.flatMap(layer => layer.effects || []).some(effect => effect.type === 'radial-gradient')).toBe(true);
|
|
});
|