Organize test logs into separate directories by test suite and file
- wxWidgets logs: tests/logs/wxwidgets/<test-file>/ - KiCad logs: tests/logs/kicad/<test-file>/ - Global setup now cleans all log subdirectories recursively - Added globalSetup to KiCad playwright config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2ad6786198
commit
eedfe22654
6 changed files with 88 additions and 31 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { test as base } from '@playwright/test';
|
import { test as base } from '@playwright/test';
|
||||||
import { setupTestLogger, writeTestLogs, TestLogger, MAIN_CANVAS, waitForApp, tryLoadApp, getCanvasBox } from './test-utils';
|
import * as path from 'path';
|
||||||
|
import { setupTestLogger, writeTestLogs, TestLogger, MAIN_CANVAS, waitForApp, tryLoadApp, getCanvasBox, WXWIDGETS_LOGS_DIR, getTestFileName } from './test-utils';
|
||||||
|
|
||||||
// Extend base test with automatic logging
|
// Extend base test with automatic logging
|
||||||
export const test = base.extend<{
|
export const test = base.extend<{
|
||||||
|
|
@ -13,8 +14,10 @@ export const test = base.extend<{
|
||||||
|
|
||||||
await use(logger);
|
await use(logger);
|
||||||
|
|
||||||
// Write logs after test completes
|
// Write logs to wxwidgets/<test-file>/ directory
|
||||||
writeTestLogs(testName, logger);
|
const testFileName = getTestFileName(testInfo.file);
|
||||||
|
const logsDir = path.join(WXWIDGETS_LOGS_DIR, testFileName);
|
||||||
|
writeTestLogs(testName, logger, logsDir);
|
||||||
logger.cleanup();
|
logger.cleanup();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export const MAIN_CANVAS = '#canvas';
|
export const MAIN_CANVAS = '#canvas';
|
||||||
export const LOGS_DIR = path.join(__dirname, '..', '..', 'logs');
|
export const LOGS_BASE_DIR = path.join(__dirname, '..', '..', 'logs');
|
||||||
|
export const WXWIDGETS_LOGS_DIR = path.join(LOGS_BASE_DIR, 'wxwidgets');
|
||||||
|
export const KICAD_LOGS_DIR = path.join(LOGS_BASE_DIR, 'kicad');
|
||||||
|
|
||||||
export interface TestLogger {
|
export interface TestLogger {
|
||||||
consoleLogs: string[];
|
consoleLogs: string[];
|
||||||
|
|
@ -12,9 +14,9 @@ export interface TestLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure logs directory exists
|
// Ensure logs directory exists
|
||||||
export function ensureLogsDir() {
|
export function ensureLogsDir(dir: string = LOGS_BASE_DIR) {
|
||||||
if (!fs.existsSync(LOGS_DIR)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,8 +50,11 @@ export function setupTestLogger(page: Page): TestLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write logs to files after test completion
|
// Write logs to files after test completion
|
||||||
export function writeTestLogs(testName: string, logger: TestLogger) {
|
export function writeTestLogs(testName: string, logger: TestLogger, logsDir: string) {
|
||||||
ensureLogsDir();
|
// Ensure logs directory exists
|
||||||
|
if (!fs.existsSync(logsDir)) {
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
// Sanitize test name for filesystem
|
// Sanitize test name for filesystem
|
||||||
const safeTestName = testName
|
const safeTestName = testName
|
||||||
|
|
@ -58,17 +63,22 @@ export function writeTestLogs(testName: string, logger: TestLogger) {
|
||||||
.replace(/^-+|-+$/g, '');
|
.replace(/^-+|-+$/g, '');
|
||||||
|
|
||||||
// Always write console log file
|
// Always write console log file
|
||||||
const logFile = path.join(LOGS_DIR, `${safeTestName}.log`);
|
const logFile = path.join(logsDir, `${safeTestName}.log`);
|
||||||
fs.writeFileSync(logFile, logger.consoleLogs.join('\n'));
|
fs.writeFileSync(logFile, logger.consoleLogs.join('\n'));
|
||||||
|
|
||||||
// Only write error file if there are errors (excluding favicon)
|
// Only write error file if there are errors (excluding favicon)
|
||||||
const realErrors = logger.errors.filter(e => !e.includes('favicon'));
|
const realErrors = logger.errors.filter(e => !e.includes('favicon'));
|
||||||
if (realErrors.length > 0) {
|
if (realErrors.length > 0) {
|
||||||
const errorFile = path.join(LOGS_DIR, `${safeTestName}.errors.log`);
|
const errorFile = path.join(logsDir, `${safeTestName}.errors.log`);
|
||||||
fs.writeFileSync(errorFile, realErrors.join('\n\n'));
|
fs.writeFileSync(errorFile, realErrors.join('\n\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to get test file name without extension
|
||||||
|
export function getTestFileName(filePath: string): string {
|
||||||
|
return path.basename(filePath, '.spec.ts');
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to wait for app initialization
|
// Helper to wait for app initialization
|
||||||
export async function waitForApp(page: Page, timeout = 30000) {
|
export async function waitForApp(page: Page, timeout = 30000) {
|
||||||
await page.waitForSelector(MAIN_CANVAS, { state: 'visible', timeout });
|
await page.waitForSelector(MAIN_CANVAS, { state: 'visible', timeout });
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,34 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively clean all files in a directory (keeps directory structure).
|
||||||
|
*/
|
||||||
|
function cleanDirectory(dir: string): number {
|
||||||
|
let count = 0;
|
||||||
|
if (!fs.existsSync(dir)) return count;
|
||||||
|
|
||||||
|
for (const entry of fs.readdirSync(dir)) {
|
||||||
|
const fullPath = path.join(dir, entry);
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
if (stat.isFile()) {
|
||||||
|
fs.unlinkSync(fullPath);
|
||||||
|
count++;
|
||||||
|
} else if (stat.isDirectory()) {
|
||||||
|
count += cleanDirectory(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global setup for Playwright tests.
|
* Global setup for Playwright tests.
|
||||||
* Cleans the logs directory before each test run to prevent stale logs.
|
* Cleans the logs directory (and subdirectories) before each test run.
|
||||||
*/
|
*/
|
||||||
export default async function globalSetup() {
|
export default async function globalSetup() {
|
||||||
const logsDir = path.join(__dirname, 'logs');
|
const logsDir = path.join(__dirname, 'logs');
|
||||||
|
const count = cleanDirectory(logsDir);
|
||||||
if (fs.existsSync(logsDir)) {
|
if (count > 0) {
|
||||||
// Remove all files in logs directory
|
console.log(`[global-setup] Removed ${count} log files from ${logsDir}`);
|
||||||
for (const file of fs.readdirSync(logsDir)) {
|
|
||||||
const filePath = path.join(logsDir, file);
|
|
||||||
// Only remove files, not subdirectories
|
|
||||||
if (fs.statSync(filePath).isFile()) {
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`[global-setup] Cleaned ${logsDir}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
tests/kicad/fixtures.ts
Normal file
26
tests/kicad/fixtures.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { test as base } from '@playwright/test';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { setupTestLogger, writeTestLogs, TestLogger, MAIN_CANVAS, waitForApp, tryLoadApp, getCanvasBox, KICAD_LOGS_DIR, getTestFileName } from '../e2e/utils/test-utils';
|
||||||
|
|
||||||
|
// Extend base test with automatic logging
|
||||||
|
export const test = base.extend<{
|
||||||
|
testLogger: TestLogger;
|
||||||
|
}>({
|
||||||
|
testLogger: async ({ page }, use, testInfo) => {
|
||||||
|
// Build test name from describe block + test title
|
||||||
|
const testName = testInfo.titlePath.join(' - ');
|
||||||
|
|
||||||
|
const logger = setupTestLogger(page);
|
||||||
|
|
||||||
|
await use(logger);
|
||||||
|
|
||||||
|
// Write logs to kicad/<test-file>/ directory
|
||||||
|
const testFileName = getTestFileName(testInfo.file);
|
||||||
|
const logsDir = path.join(KICAD_LOGS_DIR, testFileName);
|
||||||
|
writeTestLogs(testName, logger, logsDir);
|
||||||
|
logger.cleanup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { expect } from '@playwright/test';
|
||||||
|
export { MAIN_CANVAS, waitForApp, tryLoadApp, getCanvasBox };
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PCBnew WASM E2E Tests
|
* PCBnew WASM E2E Tests
|
||||||
*
|
*
|
||||||
* These tests verify that the KiCad PCBnew application runs correctly in the browser.
|
* These tests verify that the KiCad PCBnew application runs correctly in the browser.
|
||||||
* The WASM files are served from apps/kicad/ via the web server.
|
* The WASM files are served from apps/kicad/ via the web server.
|
||||||
|
*
|
||||||
|
* Logs are written to tests/logs/:
|
||||||
|
* - {test-name}.log - console output
|
||||||
|
* - {test-name}.errors.log - page errors (if any)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
test.describe('PCBnew WASM', () => {
|
test.describe('PCBnew WASM', () => {
|
||||||
|
|
@ -13,21 +17,22 @@ test.describe('PCBnew WASM', () => {
|
||||||
await page.goto('/kicad/pcbnew.html');
|
await page.goto('/kicad/pcbnew.html');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should load PCBnew WASM module', async ({ page }) => {
|
test('should load PCBnew WASM module', async ({ page, testLogger }) => {
|
||||||
// Wait for WASM to initialize (look for canvas or status indicator)
|
// Wait for WASM to initialize (look for canvas or status indicator)
|
||||||
// This is a basic smoke test - expand as PCBnew integration matures
|
// This is a basic smoke test - expand as PCBnew integration matures
|
||||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60000 });
|
await expect(page.locator('canvas')).toBeVisible({ timeout: 60000 });
|
||||||
|
|
||||||
|
// testLogger automatically captures all console output and errors
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render without JavaScript errors', async ({ page }) => {
|
test('should render without JavaScript errors', async ({ page, testLogger }) => {
|
||||||
const errors: string[] = [];
|
|
||||||
page.on('pageerror', err => errors.push(err.message));
|
|
||||||
|
|
||||||
await page.goto('/kicad/pcbnew.html');
|
await page.goto('/kicad/pcbnew.html');
|
||||||
await page.waitForTimeout(5000); // Give time for WASM to load
|
await page.waitForTimeout(5000); // Give time for WASM to load
|
||||||
|
|
||||||
// Filter out known acceptable errors if any
|
// Check captured errors (excluding known acceptable ones)
|
||||||
const criticalErrors = errors.filter(e => !e.includes('ResizeObserver'));
|
const criticalErrors = testLogger.errors.filter(e =>
|
||||||
|
!e.includes('ResizeObserver') && !e.includes('favicon')
|
||||||
|
);
|
||||||
expect(criticalErrors).toHaveLength(0);
|
expect(criticalErrors).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ function findFreePort(): number {
|
||||||
const port = getOrFindPort();
|
const port = getOrFindPort();
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
globalSetup: './global-setup.ts',
|
||||||
testDir: './kicad',
|
testDir: './kicad',
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue