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
|
test_output_full.txt
|
||||||
benchmark-after.txt
|
benchmark-after.txt
|
||||||
benchmark-baseline.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",
|
"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: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: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:list": "node bin/torch-lock.mjs list-memories",
|
||||||
"torch:memory:inspect": "node bin/torch-lock.mjs inspect-memory",
|
"torch:memory:inspect": "node bin/torch-lock.mjs inspect-memory",
|
||||||
"torch:remove": "node bin/torch-lock.mjs remove"
|
"torch:remove": "node bin/torch-lock.mjs remove"
|
||||||
@@ -77,6 +80,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
|
"c8": "^10.1.3",
|
||||||
"eslint": "^10.0.1",
|
"eslint": "^10.0.1",
|
||||||
"globals": "^17.3.0"
|
"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,
|
dryRun: false,
|
||||||
force: false,
|
force: false,
|
||||||
port: DEFAULT_DASHBOARD_PORT,
|
port: DEFAULT_DASHBOARD_PORT,
|
||||||
host: null,
|
host: undefined,
|
||||||
logDir: 'task-logs',
|
logDir: 'task-logs',
|
||||||
ignoreLogs: false,
|
ignoreLogs: false,
|
||||||
id: null,
|
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 packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const auth = await getDashboardAuth();
|
const auth = await getDashboardAuth();
|
||||||
|
|
||||||
|
let gracefulShutdown;
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Basic Auth check
|
// 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}`);
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
let pathname = url.pathname;
|
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/
|
// Redirect / to /dashboard/
|
||||||
if (pathname === '/' || pathname === '/dashboard') {
|
if (pathname === '/' || pathname === '/dashboard') {
|
||||||
res.writeHead(302, { ...SECURITY_HEADERS, 'Location': '/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(`Serving files from ${packageRoot}`);
|
||||||
console.log(`Using configuration from ${process.cwd()}`);
|
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);
|
resolve(server);
|
||||||
});
|
});
|
||||||
server.on('error', reject);
|
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