Files
pgames-manager/frontend/tests/upload-panel-browser.test.mjs
mrleo1nid db52a2b589
CI / frontend (push) Failing after 29s
CI / backend (push) Failing after 37s
feat: collapse background uploads into a progress circle and fit hero covers
2026-10-26 18:00:26 +03:00

113 lines
8.0 KiB
JavaScript

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('<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500"><rect width="1200" height="500" fill="#273d5c"/><rect width="80" height="500" fill="#f90"/><rect x="1120" width="80" height="500" fill="#0b9"/><text x="600" y="290" text-anchor="middle" fill="white" font-size="100">FULL BANNER</text></svg>'); 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('<!doctype html><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/styles.css"><main></main><script src="/bundle.js"></script>');
});
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));
}
});