Record plugin page mobile and desktop screenshot evidence

This commit is contained in:
Danielv123 committed 2026-11-15 18:27:01 +01:00
commit 8fd140f767
10 files changed
+178

No files matched your search

+18
View File
@@ -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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+14
View File
@@ -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();
+57
View File
@@ -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();
+2
View File
@@ -0,0 +1,2 @@
<!doctype html>
<html><head><meta name="viewport" content="width=device-width, initial-scale=1"><title>Plugin layout fixture</title></head><body><div id="root"></div><script>window.webRoot = "/";</script><script type="module" src="/temp/plugin-layout/main.tsx"></script></body></html>
+63
View File
@@ -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: <div>{title}</div> })),
{ path: "/plugins", sidebarName: "Plugins", content: <PluginsPage /> },
{ path: "/plugins/:name/view", content: <div>Plugin navigation target</div> },
],
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(<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
<ControlContext.Provider value={control}><BrowserRouter><SiteLayout /></BrowserRouter></ControlContext.Provider>
</ConfigProvider>);
+24
View File
@@ -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();