// 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('
'); } }); 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)); } });