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("F95 personal cookies: decline, expired session, retry, delete and no parent form submission", { 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, useState} from 'react'; import {createRoot} from 'react-dom/client'; import {F95Fetch} from './ui/F95Fetch'; window.submits = 0; window.loaded = []; function App() { const [title, setTitle] = useState('Manual title'); return h('form', {onSubmit: e => {e.preventDefault(); window.submits++;}}, h(F95Fetch, {onLoaded: m => {window.loaded.push(m); setTitle(m.title);}}), h('output', null, title)); } createRoot(document.querySelector('main')).render(h(App));`, resolveDir: fileURLToPath(new URL("../src/", import.meta.url)), loader: "tsx" }, bundle: true, write: false, format: "iife", platform: "browser", logLevel: "silent" }); let imports = 0, deleted = 0; const saved = []; const css = readFileSync(new URL("../src/styles.css", import.meta.url), "utf8"); const server = createServer(async (req, res) => { if (req.url === "/styles.css") { res.setHeader("Content-Type", "text/css"); res.end(css); return; } if (req.url === "/bundle.js") { res.setHeader("Content-Type", "application/javascript"); res.end(compiled.outputFiles[0].text); return; } if (req.url === "/api/metadata/f95/session") { if (req.method === "PUT") { let data = ""; for await (const chunk of req) data += chunk; saved.push(JSON.parse(data)); } else if (req.method === "DELETE") deleted++; res.writeHead(204); res.end(); return; } if (req.url === "/api/metadata/f95") { imports++; const authenticated = imports === 4; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({url: "https://f95zone.to/threads/example.123/", title: "Imported title", description: "Story", tags: [], cover_hash: authenticated ? "cover.webp" : null, cover_warning: authenticated ? null : "Нужна авторизация", auth_required: !authenticated, metadata_available: imports !== 5})); 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"}); page.setDefaultTimeout(10000); await page.goto(`http://127.0.0.1:${server.address().port}`); await page.getByPlaceholder("https://f95zone.to/threads/…").fill("https://f95zone.to/threads/example.123/"); await page.getByRole("button", {name: "Подтянуть", exact: true}).click(); const dialog = page.getByRole("dialog"); await dialog.waitFor(); assert.equal(await page.locator("output").textContent(), "Imported title"); if (process.env.PGAMES_F95_SCREENSHOT_DIR) { await page.setViewportSize({width: 676, height: 743}); await page.screenshot({path: `${process.env.PGAMES_F95_SCREENSHOT_DIR}/f95-collapsed.png`}); } // Exercise the actual stylesheet, expanded help and the overflow shown in the report. await dialog.getByText("Где взять cookie", {exact: true}).click(); await dialog.getByText("Если F95zone блокирует запрос", {exact: true}).click(); for (const viewport of [{width: 676, height: 743}, {width: 320, height: 568}]) { await page.setViewportSize(viewport); const layout = await dialog.evaluate(element => { const box = element.getBoundingClientRect(); const buttons = [...element.querySelectorAll('.modal-actions button')].map(button => button.getBoundingClientRect()); return {insideViewport: box.left >= 0 && box.right <= innerWidth && box.top >= 0 && box.bottom <= innerHeight, noHorizontalOverflow: element.scrollWidth <= element.clientWidth + 1, buttonsInside: buttons.every(button => button.left >= box.left && button.right <= box.right)}; }); assert.deepEqual(layout, {insideViewport: true, noHorizontalOverflow: true, buttonsInside: true}); await dialog.getByRole("button", {name: "Пропустить", exact: true}).scrollIntoViewIfNeeded(); if (process.env.PGAMES_F95_SCREENSHOT_DIR) { await page.screenshot({path: `${process.env.PGAMES_F95_SCREENSHOT_DIR}/f95-${viewport.width}.png`}); } } await page.setViewportSize({width: 1000, height: 900}); await dialog.getByRole("button", {name: "Пропустить", exact: true}).press("Enter"); await dialog.waitFor({state: "detached"}); assert.equal(imports, 1); assert.equal(saved.length, 0); assert.equal(await page.locator("output").textContent(), "Imported title"); await page.getByRole("button", {name: "Подтянуть", exact: true}).click(); await dialog.waitFor(); await dialog.getByLabel("xf_user", {exact: true}).fill("expired"); await dialog.getByRole("button", {name: "Сохранить и повторить"}).click(); await page.waitForFunction(() => window.loaded.length === 3); assert.equal(await dialog.count(), 1, "expired cookies keep the replacement dialog open"); assert.equal(await dialog.getByLabel("xf_user", {exact: true}).inputValue(), ""); await dialog.getByLabel("xf_user", {exact: true}).fill("fresh"); await dialog.getByLabel("xf_session (необязательно)").fill("example"); await dialog.getByText("Если F95zone блокирует запрос", {exact: true}).click(); await dialog.getByLabel("User-Agent (необязательно)").fill("Test browser"); await dialog.getByLabel("xf_user", {exact: true}).press("Enter"); await dialog.waitFor({state: "detached"}); assert.deepEqual(saved, [{xf_user: "expired", xf_session: "", cf_clearance: "", user_agent: ""}, {xf_user: "fresh", xf_session: "example", cf_clearance: "", user_agent: "Test browser"}]); assert.equal(await page.evaluate(() => window.loaded.at(-1).cover_hash), "cover.webp"); assert.equal(await page.evaluate(() => window.submits), 0); assert.equal(await page.evaluate(() => localStorage.length), 0); await page.getByText("Настройки F95zone", {exact: true}).click(); await page.getByRole("button", {name: "Забыть cookie F95zone"}).click(); await page.getByText("Ваши сохранённые cookie F95zone удалены.").waitFor(); assert.equal(deleted, 1); await page.reload(); await page.getByPlaceholder("https://f95zone.to/threads/…").fill("https://f95zone.to/threads/example.123/"); await page.getByRole("button", {name: "Подтянуть", exact: true}).click(); await dialog.waitFor(); assert.equal(await page.locator("output").textContent(), "Manual title", "login-only response cannot erase manual fields"); await dialog.getByLabel("xf_user", {exact: true}).press("Escape"); await dialog.waitFor({state: "detached"}); assert.equal(imports, 5); assert.equal(saved.length, 2); } finally { await browser?.close(); await new Promise(resolve => server.close(resolve)); } }); test("new game shows an unsaved F95 cover and aligns metadata fields", { 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} from 'react-router-dom'; import {TranslationRunProvider} from './translator/runner'; import {UploadProvider} from './uploads'; import {NewGame} from './pages/NewGame'; createRoot(document.querySelector('main')).render(h(MemoryRouter, {initialEntries: ['/games/new']}, h(TranslationRunProvider, null, h(UploadProvider, null, h(NewGame)))));`, 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 blobRequests = 0; const server = createServer((req, res) => { if (req.url === "/styles.css") { res.setHeader("Content-Type", "text/css"); res.end(css); return; } if (req.url === "/bundle.js") { res.setHeader("Content-Type", "application/javascript"); res.end(compiled.outputFiles[0].text); return; } if (req.url.startsWith("/blobs/")) { blobRequests++; res.writeHead(404); res.end(); return; } if (req.url === "/api/metadata/f95") { res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({url: "https://f95zone.to/threads/example.123/", title: "Mist", description: "A story", tags: ["adventure", "survival"], cover_hash: "a".repeat(64) + ".webp", cover_preview: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jCTkAAAAASUVORK5CYII=", cover_warning: null, auth_required: false, metadata_available: true})); 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: 947, height: 870}}); page.setDefaultTimeout(10000); await page.goto(`http://127.0.0.1:${server.address().port}`); await page.locator('input[type="file"]').setInputFiles({name: "MIST-1.0.3-pc.zip", mimeType: "application/zip", buffer: Buffer.from("archive")}); await page.getByPlaceholder("https://f95zone.to/threads/…").fill("https://f95zone.to/threads/example.123/"); await page.getByRole("button", {name: "Подтянуть", exact: true}).click(); await page.getByText("Данные из F95zone загружены", {exact: true}).waitFor(); await page.getByRole("img", {name: "Обложка игры из F95zone"}).evaluate(image => image.decode()); assert.equal(blobRequests, 0, "unsaved covers must not use a route that requires a saved game"); assert.equal(await page.getByRole("textbox", {name: "Название", exact: true}).inputValue(), "Mist"); assert.equal(await page.getByRole("textbox", {name: /^Теги/}).inputValue(), "adventure, survival"); const title = await page.getByRole("textbox", {name: "Название", exact: true}).boundingBox(); const tags = await page.getByRole("textbox", {name: /^Теги/}).boundingBox(); assert.ok(Math.abs(title.y - tags.y) < 1, "name and tags inputs align despite the tags hint"); for (const width of [947, 375]) { await page.setViewportSize({width, height: 870}); 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}/f95-result-${width}.png`, fullPage: true}); } } await page.getByRole("button", {name: "Убрать обложку", exact: true}).click(); assert.equal(await page.getByRole("img", {name: "Обложка игры из F95zone"}).count(), 0); assert.equal(await page.getByRole("textbox", {name: "Название", exact: true}).inputValue(), "Mist"); } finally { await browser?.close(); await new Promise(resolve => server.close(resolve)); } });