mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-09 00:12:21 +02:00
192 lines
8.2 KiB
JavaScript
192 lines
8.2 KiB
JavaScript
const fs = require('node:fs');
|
|
const { test, expect } = require('@playwright/test');
|
|
const {
|
|
compareExportPixels,
|
|
encodedImagePixelDigest,
|
|
editorState,
|
|
flattenedPixelDigest,
|
|
openBlankEditor,
|
|
openExportDialog,
|
|
reopenDraft,
|
|
waitForDraft,
|
|
} = require('./helpers.js');
|
|
|
|
async function seedGradient(page) {
|
|
await page.evaluate(async () => {
|
|
const { state } = await import('/static/js/editor/state.js');
|
|
const layer = state.layers.find(item => item.id === state.activeLayerId);
|
|
const gradient = layer.ctx.createLinearGradient(0, 0, layer.canvas.width, layer.canvas.height);
|
|
gradient.addColorStop(0, '#d43f67');
|
|
gradient.addColorStop(0.5, '#4aa884');
|
|
gradient.addColorStop(1, '#3868d4');
|
|
layer.ctx.fillStyle = gradient;
|
|
layer.ctx.fillRect(0, 0, layer.canvas.width, layer.canvas.height);
|
|
window.galleryEditorComposite?.();
|
|
});
|
|
}
|
|
|
|
async function downloadFormat(page, format, filename) {
|
|
await openExportDialog(page);
|
|
await page.locator(`[data-format="${format}"]`).click();
|
|
await page.locator('#ge-export-filename').fill(filename);
|
|
const downloadPromise = page.waitForEvent('download');
|
|
await page.locator('.ge-export-dialog button[type="submit"]').click();
|
|
const download = await downloadPromise;
|
|
return {
|
|
name: download.suggestedFilename(),
|
|
bytes: fs.readFileSync(await download.path()),
|
|
};
|
|
}
|
|
|
|
async function addAdjustment(page, type) {
|
|
await page.locator('#ge-add-layer').click();
|
|
await expect(page.locator('.ge-add-layer-menu')).toBeVisible();
|
|
await page.locator(`.ge-add-layer-menu [data-adjustment-type="${type}"]`).click();
|
|
await expect(page.locator('.ge-adj-popup')).toBeVisible();
|
|
}
|
|
|
|
async function exportCurrentPng(page) {
|
|
const dataUrl = await page.evaluate(async () => {
|
|
const editor = await import('/static/js/galleryEditor.js');
|
|
return editor.exportPNG();
|
|
});
|
|
return Buffer.from(dataUrl.split(',', 2)[1], 'base64');
|
|
}
|
|
|
|
test('every retained adjustment family exports the visible composite exactly', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 128, height: 96 }, 'Adjustment export matrix');
|
|
await seedGradient(page);
|
|
|
|
const cases = [
|
|
['brightness-contrast', 'brightness', '35'],
|
|
['exposure', 'exposure', '35'],
|
|
['white-balance', 'temperature', '30'],
|
|
['hue-saturation', 'hue', '30'],
|
|
['vibrance', 'vibrance', '45'],
|
|
['black-white', 'red', '60'],
|
|
['shadows-highlights', 'shadows', '35'],
|
|
['levels', 'inBlack', '20'],
|
|
['color-balance', 'shadows-r', '35'],
|
|
['selective-color', 'cyan', '30'],
|
|
];
|
|
|
|
for (const [type, key, value] of cases) {
|
|
await addAdjustment(page, type);
|
|
const control = page.locator(`.ge-adj-row input[data-key="${key}"]`);
|
|
await control.fill(value);
|
|
await control.dispatchEvent('input');
|
|
await page.locator('[data-adj-action="ok"]').click();
|
|
const visibleDigest = await flattenedPixelDigest(page);
|
|
expect(await encodedImagePixelDigest(page, await exportCurrentPng(page))).toEqual(visibleDigest);
|
|
}
|
|
|
|
await addAdjustment(page, 'curves');
|
|
const curve = page.locator('.ge-curves-canvas');
|
|
const curveBox = await curve.boundingBox();
|
|
await page.mouse.click(curveBox.x + curveBox.width * 0.5, curveBox.y + curveBox.height * 0.28);
|
|
await page.locator('[data-adj-action="ok"]').click();
|
|
const curveDigest = await flattenedPixelDigest(page);
|
|
expect(await encodedImagePixelDigest(page, await exportCurrentPng(page))).toEqual(curveDigest);
|
|
|
|
await addAdjustment(page, 'gradient-map');
|
|
await page.locator('[data-gradient-key="shadows"]').fill('#102030');
|
|
await page.locator('[data-gradient-key="shadows"]').dispatchEvent('input');
|
|
await page.locator('[data-adj-action="ok"]').click();
|
|
const gradientDigest = await flattenedPixelDigest(page);
|
|
expect(await encodedImagePixelDigest(page, await exportCurrentPng(page))).toEqual(gradientDigest);
|
|
});
|
|
|
|
test('composited correction exports preserve pixels and format metadata', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 160, height: 120 }, 'Export fidelity');
|
|
await seedGradient(page);
|
|
|
|
await page.locator('#ge-add-layer').click();
|
|
await page.locator('.ge-add-layer-menu [data-adjustment-type="exposure"]').click();
|
|
await page.locator('.ge-adj-row input[data-key="exposure"]').fill('35');
|
|
await page.locator('.ge-adj-row input[data-key="exposure"]').dispatchEvent('input');
|
|
await page.locator('[data-adj-action="ok"]').click();
|
|
const renderedPixels = await flattenedPixelDigest(page);
|
|
|
|
const png = await downloadFormat(page, 'png', 'correction');
|
|
expect(png.name).toBe('correction.png');
|
|
expect(png.bytes.subarray(1, 4).toString('ascii')).toBe('PNG');
|
|
expect(png.bytes.readUInt32BE(16)).toBe(160);
|
|
expect(png.bytes.readUInt32BE(20)).toBe(120);
|
|
expect(await encodedImagePixelDigest(page, png.bytes)).toEqual(renderedPixels);
|
|
|
|
const jpeg = await downloadFormat(page, 'jpeg', 'correction-jpeg');
|
|
expect(jpeg.name).toBe('correction-jpeg.jpg');
|
|
expect(jpeg.bytes[0]).toBe(0xff);
|
|
expect(jpeg.bytes[1]).toBe(0xd8);
|
|
|
|
const webp = await downloadFormat(page, 'webp', 'correction-webp');
|
|
expect(webp.name).toBe('correction-webp.webp');
|
|
expect(webp.bytes.subarray(0, 4).toString('ascii')).toBe('RIFF');
|
|
expect(webp.bytes.subarray(8, 12).toString('ascii')).toBe('WEBP');
|
|
|
|
const comparison = await compareExportPixels(page, png.bytes, { width: 160, height: 120 });
|
|
expect(comparison.meanAbsoluteError).toBe(0);
|
|
expect(comparison.maximumError).toBe(0);
|
|
|
|
const exposure = (await editorState(page)).layers.find(layer => layer.kind === 'adjustment');
|
|
const exposureRow = page.locator(`.ge-layer-item[data-layer-id="${exposure.id}"]`);
|
|
await exposureRow.locator('.ge-layer-opacity').fill('62');
|
|
await exposureRow.locator('.ge-layer-opacity').dispatchEvent('input');
|
|
const opacityState = await editorState(page);
|
|
expect(opacityState.layers.find(layer => layer.id === exposure.id).opacity).toBeCloseTo(.62, 2);
|
|
const opacityPixels = await flattenedPixelDigest(page);
|
|
|
|
const opacityPng = await downloadFormat(page, 'png', 'correction-opacity');
|
|
expect(await encodedImagePixelDigest(page, opacityPng.bytes)).toEqual(opacityPixels);
|
|
expect(await compareExportPixels(page, opacityPng.bytes, { width: 160, height: 120 })).toEqual({
|
|
meanAbsoluteError: 0,
|
|
maximumError: 0,
|
|
changedPixelRatio: 0,
|
|
});
|
|
|
|
const draftId = await waitForDraft(page);
|
|
await reopenDraft(page, draftId);
|
|
const reopened = await editorState(page);
|
|
expect(reopened.layers.find(layer => layer.id === exposure.id).opacity).toBeCloseTo(.62, 2);
|
|
expect(await flattenedPixelDigest(page)).toEqual(opacityPixels);
|
|
});
|
|
|
|
test('export preview clears stale matte pixels when transparency changes', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 160, height: 120 }, 'Export preview');
|
|
await page.evaluate(async () => {
|
|
const { state } = await import('/static/js/editor/state.js');
|
|
for (const layer of state.layers) layer.ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
|
|
const layer = state.layers.find(item => item.id === state.activeLayerId);
|
|
layer.ctx.fillStyle = '#2671c8';
|
|
layer.ctx.fillRect(40, 30, 80, 60);
|
|
window.galleryEditorComposite?.();
|
|
});
|
|
|
|
await openExportDialog(page);
|
|
const previewPixel = () => page.locator('.ge-export-preview').evaluate(canvas => (
|
|
[...canvas.getContext('2d').getImageData(0, 0, 1, 1).data]
|
|
));
|
|
expect(await previewPixel()).toEqual([0, 0, 0, 0]);
|
|
|
|
await page.locator('#ge-export-transparency').uncheck();
|
|
await page.locator('#ge-export-matte').click();
|
|
await page.locator('.cp-hex').fill('#d94747');
|
|
await page.locator('.cp-hex').press('Enter');
|
|
expect(await previewPixel()).toEqual([217, 71, 71, 255]);
|
|
|
|
await page.locator('#ge-export-transparency').check();
|
|
expect(await previewPixel()).toEqual([0, 0, 0, 0]);
|
|
await page.locator('.ge-export-close').click();
|
|
});
|
|
|
|
test('closing export returns focus to the launch control', async ({ page }) => {
|
|
await openBlankEditor(page, { width: 160, height: 120 }, 'Export focus');
|
|
const launch = page.locator('#ge-save-menu-btn');
|
|
await launch.focus();
|
|
await launch.click();
|
|
await page.locator('#ge-download').click();
|
|
await expect(page.locator('.ge-export-dialog')).toBeVisible();
|
|
await page.locator('.ge-export-close').click();
|
|
await expect.poll(() => page.evaluate(() => document.activeElement?.id)).toBe('ge-save-menu-btn');
|
|
});
|