mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 07:37:56 +09:00
212 lines
13 KiB
JavaScript
212 lines
13 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { existsSync } from 'node:fs';
|
|
import { chromium } from 'playwright';
|
|
import { createServer } from 'vite';
|
|
|
|
// Real React mount with controlled HTTP/socket adapters and visibility events.
|
|
const fixture = `
|
|
import React, { useState } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { useMetrics } from '/src/hooks/useMetrics.js';
|
|
import api from '/src/services/api/index.js';
|
|
import socket from '/src/services/socket.js';
|
|
import { WorkspaceProvider } from '/src/contexts/WorkspaceContext.jsx';
|
|
import { workspaceStore } from '/src/services/workspaceStore.js';
|
|
import { useMetricHistory } from '/src/components/dashboard/widgets/useWidgetData.js';
|
|
import MetricsTab from '/src/components/service-detail/MetricsTab.jsx';
|
|
const handlers = new Map();
|
|
let hidden = false;
|
|
const state = { calls: 0, pending: [] };
|
|
api.getSystemMetrics = () => {
|
|
state.calls += 1;
|
|
return new Promise((resolve) => state.pending.push(resolve));
|
|
};
|
|
socket.socket = { connected: false };
|
|
socket.connect = () => {};
|
|
socket.on = (name, fn) => { handlers.set(name, fn); return () => handlers.delete(name); };
|
|
socket.subscribeMetrics = () => {};
|
|
socket.unsubscribeMetrics = () => {};
|
|
Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => hidden ? 'hidden' : 'visible' });
|
|
function App() {
|
|
const [options, setOptions] = useState({ enabled: true, autoRefresh: true });
|
|
const metrics = useMetrics(true, 60, options);
|
|
window.metricsFixture = {
|
|
state, setOptions, metrics,
|
|
emit: (name, data) => handlers.get(name)?.(data),
|
|
resolve: () => state.pending.splice(0).forEach((resolve) => resolve({ cpu: 42 })),
|
|
hide: (value) => { hidden = value; document.dispatchEvent(new Event('visibilitychange')); },
|
|
};
|
|
return React.createElement('output', {}, metrics.loading ? 'loading' : 'ready');
|
|
}
|
|
createRoot(document.getElementById('root')).render(React.createElement(App));
|
|
const widgetState = { calls: 0, pending: [], snapshots: [] };
|
|
api.getMetricsHistory = () => {
|
|
widgetState.calls += 1;
|
|
return new Promise((resolve) => widgetState.pending.push(resolve));
|
|
};
|
|
function Widget({ index, tick }) {
|
|
widgetState.snapshots[index] = useMetricHistory('local', '1h', tick);
|
|
return null;
|
|
}
|
|
function Widgets() {
|
|
const [tick, setTick] = useState(0);
|
|
window.widgetFixture = {
|
|
state: widgetState, refresh: () => setTick((value) => value + 1),
|
|
resolve: () => widgetState.pending.splice(0).forEach((resolve) => resolve({ points: [widgetState.calls] })),
|
|
workspace: () => workspaceStore.setActiveWorkspace({ id: 'other-workspace', name: 'Other' }),
|
|
};
|
|
return React.createElement(React.Fragment, {},
|
|
React.createElement(Widget, { index: 0, tick }), React.createElement(Widget, { index: 1, tick }));
|
|
}
|
|
createRoot(document.getElementById('widgets')).render(React.createElement(WorkspaceProvider, {}, React.createElement(Widgets)));
|
|
const serviceState = { containers: [], pending: [], python: [] };
|
|
api.getContainers = async () => ({ containers: serviceState.containers });
|
|
api.getContainerStats = (id) => new Promise((resolve, reject) => serviceState.pending.push({ id, resolve, reject }));
|
|
api.getPythonAppStatus = () => new Promise((resolve) => serviceState.python.push(resolve));
|
|
function Service() {
|
|
const [app, setApp] = useState(null);
|
|
window.serviceFixture = { state: serviceState, setApp };
|
|
return app ? React.createElement(MetricsTab, { app }) : null;
|
|
}
|
|
createRoot(document.getElementById('service')).render(React.createElement(Service));
|
|
`;
|
|
const server = await createServer({
|
|
server: { host: '127.0.0.1', port: 0, open: false },
|
|
plugins: [{
|
|
name: 'metrics-regression-fixture',
|
|
resolveId(id) { if (id === 'virtual:metrics-regression') return '\0metrics-regression'; },
|
|
load(id) { if (id === '\0metrics-regression') return fixture; },
|
|
configureServer(vite) {
|
|
vite.middlewares.use('/__metrics-regression', async (_req, res) => {
|
|
res.setHeader('Content-Type', 'text/html');
|
|
res.end(await vite.transformIndexHtml('/__metrics-regression',
|
|
'<div id="root"></div><div id="widgets"></div><div id="service"></div><script type="module">import "virtual:metrics-regression"</script>'));
|
|
});
|
|
},
|
|
}],
|
|
});
|
|
await server.listen();
|
|
const executablePath = [
|
|
process.env.CHROME_PATH, chromium.executablePath(),
|
|
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
|
].find((path) => path && existsSync(path));
|
|
let browser;
|
|
try {
|
|
browser = await chromium.launch({ ...(executablePath ? { executablePath } : {}), headless: true });
|
|
const page = await browser.newPage();
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
await page.goto(`http://127.0.0.1:${server.httpServer.address().port}/__metrics-regression`);
|
|
await page.waitForFunction(() => window.metricsFixture?.state.calls === 1);
|
|
const calls = () => page.evaluate(() => window.metricsFixture.state.calls);
|
|
const action = (fn) => page.evaluate(fn);
|
|
await page.waitForTimeout(240);
|
|
assert.equal(await calls(), 1, 'slow initial response cannot overlap fallback');
|
|
await action(() => { window.metricsFixture.metrics.refresh(); window.metricsFixture.metrics.refresh(); });
|
|
assert.equal(await calls(), 1, 'manual refresh shares initial request');
|
|
await action(() => window.metricsFixture.emit('connected'));
|
|
await action(() => window.metricsFixture.resolve());
|
|
await page.waitForTimeout(180);
|
|
assert.equal(await calls(), 1, 'connected socket stops fallback');
|
|
await action(() => window.metricsFixture.emit('disconnected'));
|
|
await page.waitForFunction(() => window.metricsFixture.state.calls === 2);
|
|
await action(() => window.metricsFixture.hide(true));
|
|
await action(() => window.metricsFixture.resolve());
|
|
await page.waitForTimeout(180);
|
|
assert.equal(await calls(), 2, 'hidden tabs suspend fallback');
|
|
await action(() => window.metricsFixture.hide(false));
|
|
await page.waitForFunction(() => window.metricsFixture.state.calls === 3);
|
|
await action(() => window.metricsFixture.setOptions({ enabled: false, autoRefresh: true }));
|
|
await page.waitForTimeout(30);
|
|
await action(() => window.metricsFixture.resolve());
|
|
await page.waitForTimeout(180);
|
|
assert.equal(await calls(), 3, 'remote selection stops local HTTP work');
|
|
await action(() => window.metricsFixture.setOptions({ enabled: true, autoRefresh: false }));
|
|
await page.waitForFunction(() => window.metricsFixture.state.calls === 4);
|
|
await action(() => window.metricsFixture.resolve());
|
|
await page.waitForTimeout(180);
|
|
assert.equal(await calls(), 4, 'refresh off loads one snapshot without polling');
|
|
await action(() => { window.metricsFixture.metrics.refresh(); window.metricsFixture.metrics.refresh(); });
|
|
await page.waitForFunction(() => window.metricsFixture.state.calls === 5);
|
|
await action(() => window.metricsFixture.resolve());
|
|
await page.waitForTimeout(120);
|
|
assert.equal(await calls(), 5, 'manual refresh while off does not restart timer');
|
|
await page.waitForFunction(() => window.widgetFixture?.state.calls === 1);
|
|
await action(() => window.widgetFixture.resolve());
|
|
await page.waitForFunction(() => window.widgetFixture.state.snapshots.every((item) => !item.loading));
|
|
await action(() => window.widgetFixture.refresh());
|
|
await page.waitForFunction(() => window.widgetFixture.state.calls === 2);
|
|
assert.deepEqual(await action(() => window.widgetFixture.state.snapshots.map((item) => item.data)),
|
|
[{ points: [1] }, { points: [1] }], 'refresh retains both widgets while sharing one next request');
|
|
await action(() => window.widgetFixture.resolve());
|
|
await page.waitForFunction(() => window.widgetFixture.state.snapshots.every((item) => !item.loading));
|
|
await action(() => window.widgetFixture.workspace());
|
|
await page.waitForFunction(() => window.widgetFixture.state.calls === 3);
|
|
assert.deepEqual(await action(() => window.widgetFixture.state.snapshots.map((item) => item.data)),
|
|
[null, null], 'workspace switching cannot display another workspace payload');
|
|
await action(() => window.widgetFixture.resolve());
|
|
await page.waitForFunction(() => window.widgetFixture.state.snapshots.every((item) => !item.loading));
|
|
// Exercise the actual service component: disappearance, rejected polls,
|
|
// runtime switches and a late response from the previous application.
|
|
await page.clock.install();
|
|
await action(() => {
|
|
window.serviceFixture.state.containers = [{ Id: 'a', Names: ['alpha'] }];
|
|
window.serviceFixture.setApp({ id: 1, name: 'alpha', app_type: 'docker' });
|
|
});
|
|
await page.waitForFunction(() => window.serviceFixture.state.pending.length === 1);
|
|
await action(() => window.serviceFixture.setApp({
|
|
id: 1, name: 'alpha', app_type: 'docker', status: 'running',
|
|
}));
|
|
await page.waitForTimeout(50);
|
|
assert.equal(await action(() => window.serviceFixture.state.pending.length), 1,
|
|
'recreated app metadata does not restart an in-flight metrics request');
|
|
await action(() => window.serviceFixture.state.pending.shift().resolve({ cpu_percent: 42 }));
|
|
await page.locator('#service .metrics-tab').waitFor();
|
|
assert.match(await page.locator('#service').innerText(), /42\.0%/);
|
|
await action(() => { window.serviceFixture.state.containers = []; });
|
|
await page.clock.fastForward(10001);
|
|
await page.locator('#service .empty-state__title').waitFor();
|
|
assert.equal(await page.locator('#service .metrics-tab').count(), 0, 'missing container clears previous stats');
|
|
await action(() => { window.serviceFixture.state.containers = [{ Id: 'a', Names: ['alpha'] }]; });
|
|
await page.clock.fastForward(10001);
|
|
await page.waitForFunction(() => window.serviceFixture.state.pending.length === 1);
|
|
await action(() => window.serviceFixture.state.pending.shift().resolve({ cpu_percent: 51 }));
|
|
await page.locator('#service .metrics-tab').waitFor();
|
|
await page.clock.fastForward(10001);
|
|
await page.waitForFunction(() => window.serviceFixture.state.pending.length === 1);
|
|
await action(() => window.serviceFixture.state.pending.shift().reject(new Error('container stopped')));
|
|
await page.locator('#service .empty-state__title').waitFor();
|
|
await page.clock.fastForward(10001);
|
|
await page.waitForFunction(() => window.serviceFixture.state.pending.length === 1);
|
|
await action(() => {
|
|
window.serviceFixture.state.containers = [{ Id: 'b', Names: ['beta'] }];
|
|
window.serviceFixture.setApp({ id: 2, name: 'beta', app_type: 'docker' });
|
|
});
|
|
await page.waitForFunction(() => window.serviceFixture.state.pending.length === 2);
|
|
assert.equal(await page.locator('#service [aria-busy="true"]').count(), 1, 'new app starts loading');
|
|
await action(() => window.serviceFixture.state.pending.pop().resolve({ cpu_percent: 17 }));
|
|
await page.locator('#service .metrics-tab').waitFor();
|
|
await action(() => window.serviceFixture.state.pending.shift().resolve({ cpu_percent: 99 }));
|
|
await page.waitForTimeout(50);
|
|
assert.match(await page.locator('#service').innerText(), /17\.0%/, 'late previous app response is ignored');
|
|
assert.doesNotMatch(await page.locator('#service').innerText(), /99\.0%/);
|
|
await action(() => window.serviceFixture.setApp({ id: 3, name: 'python', app_type: 'flask' }));
|
|
await page.waitForFunction(() => window.serviceFixture.state.python.length === 1);
|
|
assert.equal(await page.locator('#service .metrics-tab').count(), 0, 'Python switch clears Docker state');
|
|
await action(() => window.serviceFixture.state.python.shift()({ active: true, pid: 123 }));
|
|
await page.locator('#service .metrics-tab').waitFor();
|
|
await action(() => window.serviceFixture.setApp({ id: 4, name: 'other-python', app_type: 'flask' }));
|
|
await page.waitForFunction(() => window.serviceFixture.state.python.length === 1);
|
|
assert.equal(await page.locator('#service [aria-busy="true"]').count(), 1, 'Python switch also resets loading');
|
|
assert.doesNotMatch(await page.locator('#service').innerText(), /123/);
|
|
await action(() => window.serviceFixture.state.python.shift()({ active: false }));
|
|
await page.locator('#service .metrics-tab').waitFor();
|
|
assert.deepEqual(errors, []);
|
|
console.log('Service metrics regression passed: stable metadata refresh, missing container, failed poll, app switch, late response and Python state reset.');
|
|
console.log('Metrics browser regression passed: slow response, manual sharing, socket loss/reconnect, hidden tab, remote selection, refresh off; widget sharing, refresh retention, workspace isolation.');
|
|
} finally {
|
|
await browser?.close();
|
|
await server.close();
|
|
}
|