mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
feat: add Playwright integration tests with coverage and log capture
- Added `@playwright/test` and `c8` dependencies. - Configured Playwright in `playwright.config.js` to run the dashboard server. - Added `test/playwright/dashboard.spec.js` to verify dashboard functionality (loading, auth, config) and capture console logs. - Modified `src/dashboard.mjs` to support graceful shutdown via `/shutdown` endpoint (guarded by `NODE_V8_COVERAGE`) for coverage collection. - Updated `package.json` with `test:playwright` and `test:playwright:coverage` scripts. - Updated `.gitignore` to exclude coverage and test artifacts. - Fixed `host` default value in `src/cli-parser.mjs`. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
3298830449
commit
e8eec45ff7
8 files changed
+1187
-1
No files matched your search
@@ -26,3 +26,8 @@ reports/performance/benchmark-*.txt
|
||||
test_output_full.txt
|
||||
benchmark-after.txt
|
||||
benchmark-baseline.txt
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
playwright-report/
|
||||
test-results/
|
||||
Generated
+973
File diff suppressed because it is too large.
Load diff
@@ -67,6 +67,9 @@
|
||||
"validate:scheduler-failure-schema": "node --test test/scheduler-lock-failure-schema.contract.test.mjs",
|
||||
"test:ci-resilience": "node --test test/lock-acquisition-resilience.integration.test.mjs",
|
||||
"test:extended-main": "npm run test:integration:e2e && npm run test:ci-resilience",
|
||||
"test:playwright": "playwright test",
|
||||
"test:playwright:coverage": "rm -rf coverage/playwright/tmp && mkdir -p coverage/playwright/tmp && CI=true playwright test && npx c8 report --temp-directory coverage/playwright/tmp --report-dir coverage/playwright --reporter text --reporter json --all --src src",
|
||||
"test:playwright:ui": "playwright test --ui",
|
||||
"torch:memory:list": "node bin/torch-lock.mjs list-memories",
|
||||
"torch:memory:inspect": "node bin/torch-lock.mjs inspect-memory",
|
||||
"torch:remove": "node bin/torch-lock.mjs remove"
|
||||
@@ -77,6 +80,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"c8": "^10.1.3",
|
||||
"eslint": "^10.0.1",
|
||||
"globals": "^17.3.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @ts-check
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './test/playwright',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'list',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'node bin/torch-lock.mjs dashboard --port 3000',
|
||||
port: 3000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
env: {
|
||||
NODE_V8_COVERAGE: path.resolve('coverage/playwright/tmp'),
|
||||
TORCH_CONFIG_PATH: path.resolve('test/playwright/fixtures/torch-config.json'),
|
||||
TORCH_DASHBOARD_AUTH: 'admin:password' // Force auth for testing
|
||||
}
|
||||
},
|
||||
});
|
||||
+1
-1
@@ -17,7 +17,7 @@ export function parseArgs(argv) {
|
||||
dryRun: false,
|
||||
force: false,
|
||||
port: DEFAULT_DASHBOARD_PORT,
|
||||
host: null,
|
||||
host: undefined,
|
||||
logDir: 'task-logs',
|
||||
ignoreLogs: false,
|
||||
id: null,
|
||||
|
||||
@@ -37,6 +37,8 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const auth = await getDashboardAuth();
|
||||
|
||||
let gracefulShutdown;
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
// Basic Auth check
|
||||
@@ -71,6 +73,14 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
let pathname = url.pathname;
|
||||
|
||||
// Special case: /shutdown (only if coverage is enabled)
|
||||
if (pathname === '/shutdown' && process.env.NODE_V8_COVERAGE) {
|
||||
res.writeHead(200, SECURITY_HEADERS);
|
||||
res.end('Shutting down');
|
||||
gracefulShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect / to /dashboard/
|
||||
if (pathname === '/' || pathname === '/dashboard') {
|
||||
res.writeHead(302, { ...SECURITY_HEADERS, 'Location': '/dashboard/' });
|
||||
@@ -230,6 +240,26 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
}
|
||||
console.log(`Serving files from ${packageRoot}`);
|
||||
console.log(`Using configuration from ${process.cwd()}`);
|
||||
console.log(`NODE_V8_COVERAGE: ${process.env.NODE_V8_COVERAGE}`);
|
||||
|
||||
gracefulShutdown = () => {
|
||||
console.error('Shutting down dashboard server...');
|
||||
fsp.writeFile('shutdown.log', `SIGTERM received. Coverage dir: ${process.env.NODE_V8_COVERAGE}\n`).catch(() => {});
|
||||
server.closeAllConnections(); // Close existing connections (including /shutdown request)
|
||||
server.close(() => {
|
||||
console.error('Dashboard server closed.');
|
||||
process.exit(0);
|
||||
});
|
||||
// Force close after timeout if connections linger
|
||||
setTimeout(() => {
|
||||
console.error('Forcing shutdown after timeout');
|
||||
process.exit(1);
|
||||
}, 5000).unref();
|
||||
};
|
||||
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
|
||||
resolve(server);
|
||||
});
|
||||
server.on('error', reject);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Prepare logging directory
|
||||
const logDir = 'test_logs';
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir);
|
||||
}
|
||||
const logFile = path.join(logDir, 'playwright_console.log');
|
||||
// Clear previous log
|
||||
fs.writeFileSync(logFile, '');
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
// base64('admin:password') = 'YWRtaW46cGFzc3dvcmQ='
|
||||
'Authorization': 'Basic YWRtaW46cGFzc3dvcmQ='
|
||||
}
|
||||
});
|
||||
|
||||
test.describe.serial('TORCH Dashboard', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on('console', msg => {
|
||||
const logLine = `[${msg.type()}] ${msg.text()}\n`;
|
||||
fs.appendFileSync(logFile, logLine);
|
||||
// Also log to stdout so it appears in the runner output
|
||||
// console.log(`[Browser Console] ${msg.text()}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('should load the dashboard and verify title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Check for title "TORCH Dashboard" or similar. Checking regex /TORCH/ is safer.
|
||||
await expect(page).toHaveTitle(/TORCH/);
|
||||
|
||||
// Check for some basic element presence to ensure rendering happened
|
||||
// Assuming there is a header or main content
|
||||
// We can inspect dashboard/index.html to be sure
|
||||
// But generic check for body visibility is a start
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
|
||||
// Verify console log capture
|
||||
await page.evaluate(() => console.log('Test log from browser'));
|
||||
});
|
||||
|
||||
test('should fail authentication with wrong credentials', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
extraHTTPHeaders: {
|
||||
'Authorization': 'Basic YWRtaW46d3Jvbmc=' // admin:wrong
|
||||
}
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const response = await page.goto('/');
|
||||
expect(response.status()).toBe(401);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('should serve public config correctly', async ({ request }) => {
|
||||
const response = await request.get('/torch-config.json');
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const config = await response.json();
|
||||
expect(config.nostrLock.namespace).toBe('test-torch');
|
||||
// Security check: Auth should be stripped
|
||||
expect(config.dashboard.auth).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should shutdown gracefully to save coverage', async ({ request }) => {
|
||||
// Attempt to shutdown. It might fail if already down (unlikely in serial).
|
||||
try {
|
||||
// We don't expect a response because the server dies
|
||||
await request.get('/shutdown', { timeout: 1000 }).catch(() => {});
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
// Wait a bit for file write
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"nostrLock": {
|
||||
"namespace": "test-torch",
|
||||
"relays": ["wss://relay.damus.io"],
|
||||
"relayFallbacks": [],
|
||||
"ttlSeconds": 60,
|
||||
"queryTimeoutMs": 5000,
|
||||
"publishTimeoutMs": 5000,
|
||||
"minSuccessfulRelayPublishes": 1,
|
||||
"minActiveRelayPool": 1
|
||||
},
|
||||
"dashboard": {
|
||||
"auth": "admin:password",
|
||||
"defaultCadenceView": "daily",
|
||||
"defaultStatusView": "active",
|
||||
"relays": ["wss://relay.damus.io"],
|
||||
"namespace": "test-torch",
|
||||
"hashtag": "test-torch-lock"
|
||||
},
|
||||
"scheduler": {
|
||||
"firstPromptByCadence": {
|
||||
"daily": "src/prompts/daily/agent.md",
|
||||
"weekly": "src/prompts/weekly/agent.md"
|
||||
},
|
||||
"paused": {
|
||||
"daily": [],
|
||||
"weekly": []
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user