Files
pgames-manager/frontend/tests/imager-browser.test.mjs
T
mrleo1nid f7f7f5b1b3
CI / frontend (push) Failing after 26s
CI / backend (push) Failing after 1m37s
feat: add local image enhancement and lossless mod assets
2026-10-25 21:41:45 +03:00

86 lines
7.5 KiB
JavaScript

// Real browser checks for IndexedDB, Canvas alpha and actual layout; opt-in like tomie-browser.
import assert from "node:assert/strict";
import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url";
import { test } from "node:test";
import { build } from "esbuild";
test("image enhancement preserves alpha, natural layout, persistent cache and local-only live URLs in Chromium", {
skip: !process.env.PGAMES_PLAYWRIGHT_PATH,
}, async () => {
const { chromium } = await import(pathToFileURL(process.env.PGAMES_PLAYWRIGHT_PATH + "/index.mjs").href);
const compiled = await build({ stdin: { contents: `export * from './images'; export * from './cache'; export * from './presets'; export * from './live'; export * from '../player/renderer';
import { createElement } from 'react'; import { createRoot } from 'react-dom/client'; import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { ImagerSetup } from './ImagerSetup'; import { ImageEnhance } from './ImageEnhance';
export function showPage(kind) { createRoot(document.querySelector('main')).render(createElement(MemoryRouter, {initialEntries: ['/versions/7/enhance']}, createElement(Routes, null, createElement(Route, {path: '/versions/:versionId/enhance', element: createElement(kind === 'setup' ? ImagerSetup : ImageEnhance)})))); }`,
resolveDir: fileURLToPath(new URL("../src/imager/", import.meta.url)), loader: "ts" }, bundle: true, write: false, format: "iife", globalName: "Imager", platform: "browser", loader: { '.css': 'empty' }, logLevel: "silent" });
let writes = 0;
const server = createServer((req, res) => {
if (req.method === 'PUT' || req.method === 'POST') writes++;
if (req.url === '/api/versions/7/editor') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ revision: 'revision', editable: true, baseRevision: 'base', files: { 'images/sprite.webp': { hash: 'sourcehash', size: 1000 } }, document: { game: {} } })); }
else if (req.url === '/api/versions/7') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ id: 7, name: 'Тестовый мод', kind: 'mod' })); }
else if (req.url === '/api/versions/7/files/sourcehash') { res.setHeader('Content-Type', 'image/png'); res.end(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jCTkAAAAASUVORK5CYII=', 'base64')); }
else if (req.url === '/bundle.js') { res.setHeader('Content-Type', 'application/javascript'); res.end(compiled.outputFiles[0].text); }
else { res.setHeader('Content-Type', 'text/html'); res.end('<!doctype html><main></main><script src="/bundle.js"></script>'); }
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
let browser;
try {
browser = await chromium.launch({ headless: true, executablePath: process.env.PGAMES_CHROMIUM_EXECUTABLE });
const page = await browser.newPage();
await page.goto(`http://127.0.0.1:${server.address().port}`);
const result = await page.evaluate(async () => {
const c = Imager.canvas(4, 2), ctx = c.getContext('2d');
ctx.fillStyle = 'rgba(255,0,0,0.5)'; ctx.fillRect(0, 0, 2, 2);
const original = await Imager.png(c);
const prepared = await Imager.prepareImage(original, 'upscale');
let rejected = false;
try { await Imager.prepareImage(original, 'img2img'); } catch { rejected = true; }
const large = Imager.canvas(8, 4); const gc = large.getContext('2d');
gc.fillStyle = 'blue'; gc.fillRect(0, 0, 8, 4);
const image = await Imager.finishImage(await Imager.png(large), prepared);
const bitmap = await createImageBitmap(image.blob); gc.clearRect(0, 0, 8, 4); gc.drawImage(bitmap, 0, 0); bitmap.close();
const a = gc.getImageData(0, 0, 8, 4).data;
const hash = await Imager.presetHash(Imager.DEFAULT_PRESET);
const key = Imager.cacheKey(7, 'images/sprite.webp', 'sourcehash', hash);
await Imager.imageStore.put({ ...image, key, version: 7, path: 'images/sprite.webp' });
window.key = key;
const live = new Imager.LiveImages(7); live.enabled = true;
const first = live.url('images/sprite.webp', 'sourcehash', '/original');
let improved;
for (let i = 0; i < 100 && !improved; i++) { await new Promise((r) => setTimeout(r, 5)); improved = live.url('images/sprite.webp', 'sourcehash', '/original'); }
const renderer = new Imager.Renderer(document.querySelector('main'), 100, 100, { url: () => improved, assetScale: () => 2 });
const img = renderer.displayable({ file: 'sprite.webp' }); document.querySelector('main').append(img); await img.decode();
const natural = { width: img.getBoundingClientRect().width, height: img.getBoundingClientRect().height };
const sized = renderer.displayable({ file: 'sprite.webp', size: [20, 10] }); document.querySelector('main').append(sized); await sized.decode();
const explicit = { width: sized.getBoundingClientRect().width, height: sized.getBoundingClientRect().height };
let revoked = false; const revoke = URL.revokeObjectURL; URL.revokeObjectURL = (url) => { if (url === improved) revoked = true; revoke(url); };
live.destroy();
return { rejected, alpha: [a[3], a[(7 * 4) + 3]], scale: image.scale, first, improved, natural, explicit, revoked };
});
assert.equal(result.rejected, true); assert.deepEqual(result.alpha, [128, 0]); assert.equal(result.scale, 2);
assert.equal(result.first, null); assert.match(result.improved, /^blob:/); assert.equal(result.revoked, true);
assert.deepEqual(result.natural, { width: 4, height: 2 }); assert.deepEqual(result.explicit, { width: 20, height: 10 });
assert.equal(writes, 0, 'on-the-fly does not upload anything to the library');
const key = await page.evaluate(() => window.key);
await page.reload();
assert.equal(await page.evaluate(async (key) => (await Imager.imageStore.get(key)).scale, key), 2);
await page.evaluate(async () => Imager.clearVersion(7));
assert.equal(await page.evaluate(async (key) => !!await Imager.imageStore.get(key), key), false);
const css = ['../src/styles.css', '../src/imager/imager.css'].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n');
await page.addStyleTag({ content: css }); await page.evaluate(() => Imager.showPage('setup'));
await page.getByRole('heading', { name: 'Локальное улучшение графики', exact: true }).waitFor();
await page.getByRole('button', { name: 'Сохранить и использовать' }).click();
await page.getByRole('status').filter({ hasText: 'Пресет сохранён' }).waitFor();
await page.setViewportSize({ width: 390, height: 844 });
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, 'setup fits a mobile viewport');
if (process.env.PGAMES_IMAGER_SCREENSHOT) await page.screenshot({ path: process.env.PGAMES_IMAGER_SCREENSHOT, fullPage: true });
await page.reload(); await page.addStyleTag({ content: css }); await page.evaluate(() => Imager.showPage('editor'));
await page.getByRole('heading', { name: 'Улучшение графики', exact: true }).waitFor();
await page.getByRole('button', { name: 'Выбрать видимые (1)' }).click();
await page.getByRole('button', { name: 'Обработать (1)', exact: true }).waitFor();
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, 'editor fits a mobile viewport');
} finally { await browser?.close(); await new Promise((r) => server.close(r)); }
});