import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { createServer } from "node:http"; import { fileURLToPath, pathToFileURL } from "node:url"; import { test } from "node:test"; import { build } from "esbuild"; test("floating uploads collapse, keep live progress across navigation and show processing and failure", { 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: ` import {createElement as h} from 'react'; import {createRoot} from 'react-dom/client'; import {MemoryRouter, Link, Routes, Route} from 'react-router-dom'; import {AuthProvider} from './auth'; import {UiProvider} from './ui/Ui'; import {TranslationRunProvider} from './translator/runner'; import {UploadProvider, useUploads} from './uploads'; import {GamePage} from './pages/GamePage'; function Harness() { const uploads = useUploads(); return h('div', null, h('button', {onClick: () => uploads.start({gameId: 1, file: new File(['archive'], 'MIST.zip')})}, 'Начать'), h(Link, {to: '/games/1'}, 'Первая игра'), h(Link, {to: '/games/2'}, 'Вторая игра'), h(Routes, null, h(Route, {path: '/games/:gameId', element: h(GamePage)}))); } createRoot(document.querySelector('main')).render(h(MemoryRouter, {initialEntries: ['/games/1']}, h(UiProvider, null, h(AuthProvider, null, h(TranslationRunProvider, null, h(UploadProvider, null, h(Harness)))))));`, resolveDir: fileURLToPath(new URL("../src/", import.meta.url)), loader: "tsx"}, bundle: true, write: false, format: "iife", platform: "browser", logLevel: "silent"}); const css = readFileSync(new URL("../src/styles.css", import.meta.url), "utf8"); let ready = false; const server = createServer((req, res) => { if (req.url === "/bundle.js") { res.setHeader("Content-Type", "application/javascript"); res.end(compiled.outputFiles[0].text); return; } if (req.url === "/styles.css") { res.setHeader("Content-Type", "text/css"); res.end(css); return; } if (req.url === "/blobs/banner.webp") { res.setHeader("Content-Type", "image/svg+xml"); res.end('FULL BANNER'); return; } if (req.url === "/ready") { ready = true; res.end("ok"); return; } if (req.url.startsWith("/api/")) { res.setHeader("Content-Type", "application/json"); if (req.url === "/api/auth/me") res.end(JSON.stringify({id: 1, permissions: ["games.play"]})); else if (req.url.startsWith("/api/versions/")) res.end(JSON.stringify({id: 1, status: ready ? "ready" : "converting", progress: "Обработка"})); else res.end(JSON.stringify({id: Number(req.url.split("/").at(-1)), title: req.url.endsWith("/2") ? "No cover" : "Mist", tags: Array.from({length: 30}, (_, i) => `tag-${i}`), description: Array(8).fill("Long description of the game and its story.").join("\n\n"), cover_hash: req.url.endsWith("/2") ? null : "banner.webp", gallery_hashes: [], versions: [], main_version_id: null})); return; } res.setHeader("Content-Type", "text/html"); res.end('
'); }); await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); let browser; try { browser = await chromium.launch({headless: true, executablePath: process.env.PGAMES_CHROMIUM_EXECUTABLE}); const page = await browser.newPage({colorScheme: "dark", viewport: {width: 1906, height: 910}}); page.setDefaultTimeout(10000); await page.addInitScript(() => { window.XMLHttpRequest = class { upload = {}; open() {} setRequestHeader() {} send() { window.uploadRequest = this; } }; window.uploadProgress = value => window.uploadRequest.upload.onprogress({lengthComputable: true, loaded: value, total: 100}); window.uploadComplete = failed => { const xhr = window.uploadRequest; xhr.status = failed ? 500 : 201; xhr.responseText = JSON.stringify(failed ? {detail: "Test failure"} : {id: 1, status: "queued"}); xhr.onload(); }; }); await page.goto(`http://127.0.0.1:${server.address().port}`); await page.locator(".hero .cover img").evaluate(image => image.decode()); const layout = await page.locator(".hero-inner").evaluate(element => { const image = element.querySelector(".cover img"), cover = element.querySelector(".cover"), info = element.querySelector(".hero-info"); return {ratio: image.getBoundingClientRect().width / image.getBoundingClientRect().height, naturalRatio: image.naturalWidth / image.naturalHeight, topOffset: cover.getBoundingClientRect().top - info.getBoundingClientRect().top}; }); assert.ok(Math.abs(layout.ratio - layout.naturalRatio) < 0.01); assert.ok(Math.abs(layout.topOffset) < 1); await page.getByRole("button", {name: "Начать", exact: true}).click(); await page.waitForFunction(() => !!window.uploadRequest); await page.evaluate(() => window.uploadProgress(38)); await page.getByText("Загрузка архива… 38%", {exact: true}).waitFor(); await page.getByRole("button", {name: "Свернуть фоновые задачи", exact: true}).click(); await page.getByRole("button", {name: "Развернуть фоновые задачи. Прогресс: 38%", exact: true}).waitFor(); await page.evaluate(() => window.uploadProgress(65)); await page.getByRole("button", {name: "Развернуть фоновые задачи. Прогресс: 65%", exact: true}).waitFor(); await page.getByRole("link", {name: "Вторая игра", exact: true}).click(); await page.getByRole("heading", {name: "No cover", exact: true}).waitFor(); assert.equal(await page.locator(".hero .cover").evaluate(element => element.getBoundingClientRect().height > 0), true); await page.getByRole("button", {name: "Развернуть фоновые задачи. Прогресс: 65%", exact: true}).press("Enter"); await page.getByText("Загрузка архива… 65%", {exact: true}).waitFor(); await page.getByRole("button", {name: "Свернуть фоновые задачи", exact: true}).click(); await page.evaluate(() => window.uploadComplete(false)); await page.getByRole("button", {name: "Развернуть фоновые задачи. Обработка на сервере", exact: true}).waitFor(); await page.request.get(`http://127.0.0.1:${server.address().port}/ready`); await page.getByRole("button", {name: "Развернуть фоновые задачи. Фоновые задачи завершены", exact: true}).waitFor(); await page.getByRole("link", {name: "Первая игра", exact: true}).click(); await page.getByRole("heading", {name: "Mist", exact: true}).waitFor(); for (const width of [1906, 375]) { await page.setViewportSize({width, height: 910}); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); if (process.env.PGAMES_F95_SCREENSHOT_DIR) await page.screenshot({path: `${process.env.PGAMES_F95_SCREENSHOT_DIR}/hero-progress-${width}.png`, fullPage: true}); } await page.getByRole("button", {name: "Начать", exact: true}).click(); await page.evaluate(() => window.uploadComplete(true)); await page.getByRole("button", {name: "Развернуть фоновые задачи. Ошибка фоновой задачи", exact: true}).click(); await page.getByText("Test failure", {exact: true}).waitFor(); } finally { await browser?.close(); await new Promise(resolve => server.close(resolve)); } });