commit 8fd140f767de8e3d43490776282fde29a36bc031 Author: Danielv123 Date: Sun Nov 15 18:27:01 2026 +0100 Record plugin page mobile and desktop screenshot evidence diff --git "a/README.md\r" "b/README.md\r" new file mode 100644 index 00000000..1ed9076b --- /dev/null +++ "b/README.md\r" @@ -0,0 +1,18 @@ +# Plugin page layout screenshots + +These screenshots render the real `PluginsPage`, `PluginVersions`, and `SiteLayout` components with synthetic controller/host responses. +They are layout evidence, not a live cluster installation or restart test. +The fixture has two machines, a pending awf_oarc update and web load error, an Auto Restart plugin without a host entrypoint, a Global Chat update, and an up-to-date Research Sync plugin. + +Before: `61f2b0117263516b592bba0b397e887065de5c4d`. +After: `8229db75`. +Mobile viewport: 390 x 844 CSS pixels. +Desktop viewport: 1440 x 900 CSS pixels. +Both use Edge through Playwright, device scale factor 1, dark theme, the same data and default sorting. + +The screenshots are kept on a separate evidence branch so they are not part of the code PR's diff. + +To reproduce, put `main.tsx`, `index.html`, `serve.mjs`, `capture.mjs` and `check.mjs` in `temp/plugin-layout/` of a Clusterio checkout. +Run `pnpm install --no-frozen-lockfile`, `npm install --prefix temp/plugin-layout --no-package-lock --no-save playwright`, and `node temp/plugin-layout/serve.mjs` from the repository root. +With Microsoft Edge installed, run `node temp/plugin-layout/capture.mjs before` at the baseline or `node temp/plugin-layout/capture.mjs after` at the fixed commit. +Run `node temp/plugin-layout/check.mjs` at the fixed commit for viewport bounds, search, sorting, navigation, account menu and checklist checks. diff --git "a/after-desktop.png\r" "b/after-desktop.png\r" new file mode 100644 index 00000000..1d6ca8da Binary files /dev/null and "b/after-desktop.png\r" differ diff --git "a/after-mobile.png\r" "b/after-mobile.png\r" new file mode 100644 index 00000000..ec542d88 Binary files /dev/null and "b/after-mobile.png\r" differ diff --git "a/before-desktop.png\r" "b/before-desktop.png\r" new file mode 100644 index 00000000..3ed40f23 Binary files /dev/null and "b/before-desktop.png\r" differ diff --git "a/before-mobile.png\r" "b/before-mobile.png\r" new file mode 100644 index 00000000..5b20d851 Binary files /dev/null and "b/before-mobile.png\r" differ diff --git "a/capture.mjs\r" "b/capture.mjs\r" new file mode 100644 index 00000000..c0e284a2 --- /dev/null +++ "b/capture.mjs\r" @@ -0,0 +1,14 @@ +import { chromium } from "playwright"; +const browser = await chromium.launch({ channel: "msedge", headless: true }); +const page = await browser.newPage(); +page.on("pageerror", error => console.error(error)); +for (const [label, width, height] of [["desktop", 1440, 900], ["mobile", 390, 844]]) { + await page.setViewportSize({ width, height }); + await page.goto("http://127.0.0.1:5173/plugins"); + await page.getByRole("button", { name: "Update to 0.3.1", exact: true }).waitFor(); + await page.evaluate(() => document.fonts.ready); + await page.screenshot({ path: `temp/plugin-layout/${process.argv[2]}-${label}.png` }); + console.log(label, await page.evaluate(() => ({ width: innerWidth, scrollWidth: document.documentElement.scrollWidth, + rowHeights: [...document.querySelectorAll(".ant-table-tbody > tr")].map(row => row.getBoundingClientRect().height) }))); +} +await browser.close(); diff --git "a/check.mjs\r" "b/check.mjs\r" new file mode 100644 index 00000000..f1e81531 --- /dev/null +++ "b/check.mjs\r" @@ -0,0 +1,57 @@ +import assert from "node:assert/strict"; +import { chromium } from "playwright"; +const browser = await chromium.launch({ channel: "msedge", headless: true }); +const page = await browser.newPage(); +const errors = []; +page.on("pageerror", error => errors.push(error.message)); +for (const width of [320, 390, 575, 767, 768, 1024, 1440]) { + await page.setViewportSize({ width, height: 900 }); + await page.goto("http://127.0.0.1:5173/plugins"); + await page.getByRole("button", { name: "Update to 0.3.1", exact: true }).waitFor(); + const dimensions = await page.evaluate(() => ({ + width: innerWidth, scrollWidth: document.documentElement.scrollWidth, + columns: document.querySelectorAll("thead th").length, + brand: document.querySelector(".site-layout-brand").getBoundingClientRect().height, + overflow: [...document.querySelectorAll(".plugin-mobile-details, .plugin-mobile-details .ant-btn")] + .filter(element => element.scrollWidth > element.clientWidth + 1).length, + })); + assert.equal(dimensions.scrollWidth, width); + assert.equal(dimensions.columns, width < 768 ? 1 : 3); + assert.equal(dimensions.overflow, 0); + assert.ok(dimensions.brand < 50); + console.log("Layout", width, dimensions); +} +for (const width of [390, 1440]) { + await page.setViewportSize({ width, height: 900 }); + await page.goto("http://127.0.0.1:5173/plugins"); + const update = page.getByRole("button", { name: "Update to 2.1.0", exact: true }).filter({ hasNot: page.locator("[disabled]") }); + await page.locator(".ant-table-row").filter({ hasText: "Global Chat" }).getByRole("button").click(); + await page.getByRole("dialog", { name: "Update Global Chat 2.1.0" }).waitFor(); + assert.equal(new URL(page.url()).pathname, "/plugins"); + await page.getByRole("button", { name: "Cancel", exact: true }).click(); + await page.getByRole("button", { name: "Restart pending (2)" }).click(); + await page.getByRole("dialog", { name: "Restart machines with a pending restart" }).waitFor(); + await page.getByRole("button", { name: "Cancel", exact: true }).click(); + await page.getByRole("button", { name: "Account menu for Cooldude2606" }).click(); + await page.getByRole("menuitem", { name: "Cooldude2606" }).waitFor(); + await page.keyboard.press("Escape"); + await page.locator(".page-header-title").click(); + await page.locator(".ant-table-filter-trigger").click(); + await page.getByPlaceholder("Search plugins").fill("Global"); + assert.equal(await page.locator(".ant-table-row").count(), 1); + await page.getByPlaceholder("Search plugins").press("Enter"); + await page.reload(); + await page.getByRole("link", { name: "Global Chat", exact: true }).waitFor(); + assert.equal(await page.locator(".ant-table-row").count(), 1); + await page.getByRole("link", { name: "Global Chat", exact: true }).click(); + await page.getByText("Plugin navigation target").waitFor(); + await page.goto("http://127.0.0.1:5173/plugins"); + await page.getByRole("columnheader", { name: /Name/ }).click(); + await page.waitForFunction(() => document.querySelector(".ant-table-row")?.textContent.startsWith("Research Sync")); + assert.match(await page.locator(".ant-table-row").first().innerText(), /Research Sync/); + await page.locator(".ant-table-row").first().click(); + await page.getByText("Plugin navigation target").waitFor(); + console.log("Interactions passed", width); +} +assert.deepEqual(errors, []); +await browser.close(); diff --git "a/index.html\r" "b/index.html\r" new file mode 100644 index 00000000..a9b531b7 --- /dev/null +++ "b/index.html\r" @@ -0,0 +1,2 @@ + +Plugin layout fixture
diff --git "a/main.tsx\r" "b/main.tsx\r" new file mode 100644 index 00000000..eb1c24f2 --- /dev/null +++ "b/main.tsx\r" @@ -0,0 +1,63 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import { ConfigProvider, theme } from "antd"; +import { PluginDetails } from "@clusterio/lib"; +import SiteLayout from "../../packages/web_ui/src/components/SiteLayout.tsx"; +import PluginsPage from "../../packages/web_ui/src/components/PluginsPage.tsx"; +import ControlContext from "../../packages/web_ui/src/components/ControlContext.tsx"; +import "../../packages/web_ui/src/style.css"; + +const specs = [ + ["awf_oarc", "awf_oarc", "0.2.16", "0.3.1"], + ["auto_restart", "ExpGaming - Auto Restart", "7.0.5", "7.0.5"], + ["global_chat", "Global Chat", "2.0.0", "2.1.0"], + ["research_sync", "Research Sync", "2.1.0", "2.1.0"], +]; +const plugins = specs.map(([name, title, version, latest]) => new PluginDetails( + name, title, version, "loaded", undefined, `@fixture/${name}`, + name === "awf_oarc" ? latest : version, `@fixture/${name}`, +)); +const hostPlugins = plugins.map(plugin => plugin.name === "auto_restart" + ? new PluginDetails(plugin.name, plugin.title, plugin.version, "noEntrypoint", undefined, + plugin.npmPackage, plugin.version, plugin.dependencyName) : plugin); +const store = entries => { + const snapshot = [new Map(entries), true, null]; + return { subscribe: () => () => {}, getSnapshot: () => snapshot }; +}; +const meta = plugins.map(p => ({ name: p.name, version: p.version, npmPackage: p.npmPackage, + enabled: true, web: p.name === "awf_oarc" ? { error: "Fixture: web module could not load" } : {} })); +const originalFetch = window.fetch; +window.fetch = (url, ...rest) => String(url).endsWith("api/plugins") + ? Promise.resolve(new Response(JSON.stringify(meta))) : originalFetch(url, ...rest); +const permissions = ["core.plugin.list", "core.plugin.search", "core.plugin.update", "core.plugin.install", + "core.controller.restart", "core.host.restart"]; +const control = { + accountName: "Cooldude2606", accountRoles: [{ permissions }], + onAccountUpdate() {}, offAccountUpdate() {}, connector: {}, + plugins: new Map(), + pluginInfos: new Map(specs.map(([name, title]) => [name, { name, title }])), + hosts: store([[1, { id: 1, name: "Host 1", connected: true }]]), + systems: store([["controller", { plugins, canRestart: true }], [1, { plugins: hostPlugins, canRestart: true }]]), + instances: store([]), users: store([]), + pages: [ + ...["Controller", "Hosts", "Instances", "Users"].map(title => ({ path: `/${title.toLowerCase()}`, + sidebarName: title, content:
{title}
})), + { path: "/plugins", sidebarName: "Plugins", content: }, + { path: "/plugins/:name/view", content:
Plugin navigation target
}, + ], + async send(request) { + if (request.constructor.name === "PluginLatestVersionsRequest") { + return { versions: Object.fromEntries(specs.map(([name, , , latest]) => [`@fixture/${name}`, latest])) }; + } + throw new Error(`Unexpected fixture request: ${request.constructor.name}`); + }, + async sendTo(address, request) { + if (request.constructor.name === "PluginListRequest") { return address === "controller" ? plugins : hostPlugins; } + throw new Error(`Unexpected fixture request: ${request.constructor.name}`); + }, +}; +window.fixtureControl = control; +createRoot(document.getElementById("root")).render( + +); diff --git "a/serve.mjs\r" "b/serve.mjs\r" new file mode 100644 index 00000000..8733740c --- /dev/null +++ "b/serve.mjs\r" @@ -0,0 +1,24 @@ +import { createServer } from "../../packages/web_ui/node_modules/vite/dist/node/index.js"; +import { clusterioApp } from "../../packages/web_ui/vite.mjs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { createRequire } from "node:module"; +const require = createRequire(path.resolve("packages/web_ui/package.json")); +const server = await createServer({ + configFile: false, + root: process.cwd(), + resolve: { alias: ["react", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom", "react-dom/client", "react-router", "react-router/dom", "react-router-dom", "antd", "@sinclair/typebox"].map(name => ({ find: new RegExp(`^${name}$`), replacement: require.resolve(name) })) }, + plugins: [clusterioApp({ entry: "temp/plugin-layout/main.tsx" }), { + name: "fixture-html", configureServer(server) { + server.middlewares.use(async (req, res, next) => { + if (req.url?.startsWith("/plugins")) { + res.setHeader("Content-Type", "text/html"); + res.end(await server.transformIndexHtml(req.url, await fs.readFile("temp/plugin-layout/index.html", "utf8"))); + } else { next(); } + }); + }, + }], + server: { host: "127.0.0.1", port: 5173, strictPort: true }, +}); +await server.listen(); +server.printUrls();