mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
* fix(thread-page): make thread auto updates opt-in Wire `Auto` and `Update` to the same manual refresh path and cover the thread flow with an e2e harness. * fix(thread-page): address PR review findings
200 lines
6.7 KiB
JavaScript
200 lines
6.7 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import { join } from 'node:path';
|
|
import process from 'node:process';
|
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
import { chromium } from 'playwright';
|
|
|
|
const PAGE_TIMEOUT_MS = 30_000;
|
|
const SERVER_TIMEOUT_MS = 60_000;
|
|
const HARNESS_URL =
|
|
process.env.THREAD_AUTO_UPDATE_E2E_URL || `http://${process.env.THREAD_AUTO_UPDATE_E2E_HOST || '127.0.0.1'}:${process.env.THREAD_AUTO_UPDATE_E2E_PORT || '4174'}/?e2e=thread-auto-update`;
|
|
const harnessOrigin = new URL(HARNESS_URL);
|
|
const DEV_HOST = harnessOrigin.hostname;
|
|
const DEV_PORT = harnessOrigin.port || (harnessOrigin.protocol === 'https:' ? '443' : '80');
|
|
const shouldStartDevServer = process.argv.includes('--start-dev');
|
|
const viteCommand = process.platform === 'win32' ? 'vite.cmd' : 'vite';
|
|
const viteBin = join(process.cwd(), 'node_modules', '.bin', viteCommand);
|
|
|
|
const scenarios = [
|
|
{
|
|
name: 'desktop',
|
|
contextOptions: {
|
|
viewport: { width: 1440, height: 960 },
|
|
},
|
|
},
|
|
];
|
|
|
|
let devServerProcess = null;
|
|
let devServerShutdownRequested = false;
|
|
|
|
const cleanupDevServer = async () => {
|
|
if (!devServerProcess || devServerProcess.exitCode !== null || devServerProcess.killed) {
|
|
return;
|
|
}
|
|
|
|
devServerShutdownRequested = true;
|
|
devServerProcess.kill('SIGTERM');
|
|
|
|
const exited = await Promise.race([
|
|
new Promise((resolve) => {
|
|
devServerProcess?.once('exit', () => resolve(true));
|
|
}),
|
|
delay(5_000).then(() => false),
|
|
]);
|
|
|
|
if (!exited && devServerProcess && devServerProcess.exitCode === null && !devServerProcess.killed) {
|
|
devServerProcess.kill('SIGKILL');
|
|
}
|
|
};
|
|
|
|
const registerSignalHandlers = () => {
|
|
const handleSignal = async (signal) => {
|
|
await cleanupDevServer();
|
|
process.exit(signal === 'SIGINT' ? 130 : 143);
|
|
};
|
|
|
|
process.once('SIGINT', handleSignal);
|
|
process.once('SIGTERM', handleSignal);
|
|
};
|
|
|
|
const startDevServer = async () => {
|
|
devServerProcess = spawn(viteBin, ['--host', DEV_HOST, '--port', DEV_PORT, '--strictPort'], {
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
if (!devServerProcess.pid) {
|
|
throw new Error('Failed to start the dev server');
|
|
}
|
|
|
|
devServerProcess.once('exit', (code) => {
|
|
if (!devServerShutdownRequested && code !== 0 && code !== null) {
|
|
console.error(`Dev server exited early with code ${code}`);
|
|
}
|
|
});
|
|
};
|
|
|
|
const waitForServer = async (timeoutMs = SERVER_TIMEOUT_MS) => {
|
|
const startedAt = Date.now();
|
|
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
try {
|
|
const response = await fetch(HARNESS_URL, { redirect: 'manual' });
|
|
if (response.ok || response.status === 304) {
|
|
return;
|
|
}
|
|
} catch {
|
|
// Keep polling until timeout.
|
|
}
|
|
|
|
await delay(500);
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for ${HARNESS_URL}`);
|
|
};
|
|
|
|
const waitForTestIdText = async (page, testId, expectedText) => {
|
|
await page.getByTestId(testId).waitFor({ state: 'visible', timeout: PAGE_TIMEOUT_MS });
|
|
await page.waitForFunction(
|
|
({ expected, id }) => document.querySelector(`[data-testid="${id}"]`)?.textContent === expected,
|
|
{ expected: expectedText, id: testId },
|
|
{ timeout: PAGE_TIMEOUT_MS },
|
|
);
|
|
};
|
|
|
|
const runScenario = async (browser, scenario) => {
|
|
const context = await browser.newContext(scenario.contextOptions);
|
|
const page = await context.newPage();
|
|
const pageErrors = [];
|
|
const consoleErrors = [];
|
|
|
|
page.on('pageerror', (error) => pageErrors.push(error));
|
|
page.on('crash', () => pageErrors.push(new Error('Page crashed')));
|
|
page.on('console', (message) => {
|
|
if (message.type() === 'error') {
|
|
consoleErrors.push(message.text());
|
|
}
|
|
});
|
|
|
|
try {
|
|
await page.goto(HARNESS_URL, { waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT_MS });
|
|
await page.getByRole('heading', { name: 'Thread Auto Update E2E' }).waitFor({ timeout: PAGE_TIMEOUT_MS });
|
|
|
|
const autoCheckbox = page.getByLabel('Auto');
|
|
const simulateServerUpdateButton = page.getByTestId('simulate-server-update');
|
|
const updateButton = page.getByRole('button', { exact: true, name: 'update' });
|
|
|
|
if (await autoCheckbox.isChecked()) {
|
|
throw new Error(`[${scenario.name}] Auto checkbox should default to unchecked`);
|
|
}
|
|
|
|
await waitForTestIdText(page, 'visible-post-label', 'OP version 1');
|
|
await waitForTestIdText(page, 'visible-replies-count', '1');
|
|
await waitForTestIdText(page, 'visible-first-reply', 'reply 1 edited v1');
|
|
|
|
await simulateServerUpdateButton.click();
|
|
await waitForTestIdText(page, 'server-version', 'Server version 2');
|
|
await waitForTestIdText(page, 'visible-post-label', 'OP version 1');
|
|
await waitForTestIdText(page, 'visible-replies-count', '1');
|
|
await waitForTestIdText(page, 'visible-first-reply', 'reply 1 edited v1');
|
|
|
|
await updateButton.click();
|
|
await waitForTestIdText(page, 'visible-post-label', 'OP version 2');
|
|
await waitForTestIdText(page, 'visible-replies-count', '2');
|
|
await waitForTestIdText(page, 'visible-first-reply', 'reply 1 edited v2');
|
|
|
|
await autoCheckbox.check();
|
|
await simulateServerUpdateButton.click();
|
|
await waitForTestIdText(page, 'server-version', 'Server version 3');
|
|
await waitForTestIdText(page, 'visible-post-label', 'OP version 3');
|
|
await waitForTestIdText(page, 'visible-replies-count', '3');
|
|
await waitForTestIdText(page, 'visible-first-reply', 'reply 1 edited v3');
|
|
|
|
await autoCheckbox.uncheck();
|
|
await simulateServerUpdateButton.click();
|
|
await waitForTestIdText(page, 'server-version', 'Server version 4');
|
|
await waitForTestIdText(page, 'visible-post-label', 'OP version 3');
|
|
await waitForTestIdText(page, 'visible-replies-count', '3');
|
|
await waitForTestIdText(page, 'visible-first-reply', 'reply 1 edited v3');
|
|
|
|
if (pageErrors.length > 0) {
|
|
throw new Error(`[${scenario.name}] page errors:\n${pageErrors.map((error) => error.stack || error.message).join('\n\n')}`);
|
|
}
|
|
|
|
if (consoleErrors.length > 0) {
|
|
throw new Error(`[${scenario.name}] console errors:\n${consoleErrors.join('\n\n')}`);
|
|
}
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
};
|
|
|
|
const main = async () => {
|
|
registerSignalHandlers();
|
|
|
|
try {
|
|
if (shouldStartDevServer) {
|
|
const hasExistingServer = await waitForServer(3_000).then(() => true).catch(() => false);
|
|
if (!hasExistingServer) await startDevServer();
|
|
await waitForServer();
|
|
}
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
for (const scenario of scenarios) {
|
|
console.log(`Running thread auto update e2e for ${scenario.name}...`);
|
|
await runScenario(browser, scenario);
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
console.log('Thread auto update e2e passed.');
|
|
} finally {
|
|
await cleanupDevServer();
|
|
}
|
|
};
|
|
|
|
await main();
|