mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
fix: Ralph wizard uses file-based prompt to avoid screen escaping issues
- Add /api/sessions/:id/ralph-prompt/write endpoint to write prompt to @ralph_prompt.md - Wizard now writes full prompt to file, then sends simple read command to Claude - Fix session readiness check to wait for prompt character instead of notWorking flag - Add E2E test infrastructure for ralph-loop workflow (port 3190) - Add ralph-wizard-prod.mjs script for production testing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Screenshot fixture for E2E tests
|
||||
* Captures screenshots and compares against baselines for visual regression testing
|
||||
*/
|
||||
|
||||
import { Page } from 'playwright';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { PNG } from 'pngjs';
|
||||
import pixelmatch from 'pixelmatch';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SCREENSHOTS_DIR = join(__dirname, '..', 'screenshots');
|
||||
const BASELINES_DIR = join(SCREENSHOTS_DIR, 'baselines');
|
||||
const CURRENT_DIR = join(SCREENSHOTS_DIR, 'current');
|
||||
const DIFFS_DIR = join(SCREENSHOTS_DIR, 'diffs');
|
||||
|
||||
// Default threshold - percentage of pixels allowed to differ (0-1)
|
||||
const DEFAULT_THRESHOLD = 0.05; // 5%
|
||||
|
||||
export interface ScreenshotOptions {
|
||||
/** Threshold for pixel difference (0-1), default 0.05 */
|
||||
threshold?: number;
|
||||
/** Mask selectors to exclude from comparison */
|
||||
mask?: string[];
|
||||
/** Full page screenshot vs viewport only */
|
||||
fullPage?: boolean;
|
||||
}
|
||||
|
||||
export interface ScreenshotResult {
|
||||
/** Whether comparison passed */
|
||||
passed: boolean;
|
||||
/** Path to baseline image */
|
||||
baselinePath: string;
|
||||
/** Path to current image */
|
||||
currentPath: string;
|
||||
/** Path to diff image (only if comparison failed) */
|
||||
diffPath?: string;
|
||||
/** Number of different pixels */
|
||||
diffPixels?: number;
|
||||
/** Percentage of different pixels */
|
||||
diffPercent?: number;
|
||||
/** Whether this is a new baseline (no previous baseline existed) */
|
||||
newBaseline: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure screenshot directories exist
|
||||
*/
|
||||
function ensureDirectories(): void {
|
||||
for (const dir of [SCREENSHOTS_DIR, BASELINES_DIR, CURRENT_DIR, DIFFS_DIR]) {
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture a screenshot and compare against baseline
|
||||
* @param page - Playwright page
|
||||
* @param name - Screenshot name (without extension)
|
||||
* @param options - Screenshot options
|
||||
* @returns ScreenshotResult with comparison details
|
||||
*/
|
||||
export async function captureAndCompare(
|
||||
page: Page,
|
||||
name: string,
|
||||
options: ScreenshotOptions = {}
|
||||
): Promise<ScreenshotResult> {
|
||||
ensureDirectories();
|
||||
|
||||
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
||||
const baselinePath = join(BASELINES_DIR, `${name}.png`);
|
||||
const currentPath = join(CURRENT_DIR, `${name}.png`);
|
||||
const diffPath = join(DIFFS_DIR, `${name}.png`);
|
||||
|
||||
// Mask elements if specified (hide dynamic content)
|
||||
if (options.mask && options.mask.length > 0) {
|
||||
for (const selector of options.mask) {
|
||||
await page.evaluate((sel) => {
|
||||
const elements = document.querySelectorAll(sel);
|
||||
elements.forEach(el => {
|
||||
(el as HTMLElement).style.visibility = 'hidden';
|
||||
});
|
||||
}, selector);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture current screenshot
|
||||
const screenshotBuffer = await page.screenshot({
|
||||
path: currentPath,
|
||||
fullPage: options.fullPage ?? false,
|
||||
});
|
||||
|
||||
// Restore masked elements
|
||||
if (options.mask && options.mask.length > 0) {
|
||||
for (const selector of options.mask) {
|
||||
await page.evaluate((sel) => {
|
||||
const elements = document.querySelectorAll(sel);
|
||||
elements.forEach(el => {
|
||||
(el as HTMLElement).style.visibility = 'visible';
|
||||
});
|
||||
}, selector);
|
||||
}
|
||||
}
|
||||
|
||||
// If no baseline exists, create one
|
||||
if (!existsSync(baselinePath)) {
|
||||
writeFileSync(baselinePath, screenshotBuffer);
|
||||
return {
|
||||
passed: true,
|
||||
baselinePath,
|
||||
currentPath,
|
||||
newBaseline: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Load baseline and current images
|
||||
const baselineBuffer = readFileSync(baselinePath);
|
||||
const baselineImg = PNG.sync.read(baselineBuffer);
|
||||
const currentImg = PNG.sync.read(screenshotBuffer);
|
||||
|
||||
// Check dimensions match
|
||||
if (baselineImg.width !== currentImg.width || baselineImg.height !== currentImg.height) {
|
||||
// Dimensions changed - save diff and fail
|
||||
return {
|
||||
passed: false,
|
||||
baselinePath,
|
||||
currentPath,
|
||||
diffPath,
|
||||
diffPixels: baselineImg.width * baselineImg.height,
|
||||
diffPercent: 1,
|
||||
newBaseline: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Compare images
|
||||
const { width, height } = baselineImg;
|
||||
const diffImg = new PNG({ width, height });
|
||||
const diffPixels = pixelmatch(
|
||||
baselineImg.data,
|
||||
currentImg.data,
|
||||
diffImg.data,
|
||||
width,
|
||||
height,
|
||||
{ threshold: 0.1 } // pixelmatch threshold (per-pixel sensitivity)
|
||||
);
|
||||
|
||||
const totalPixels = width * height;
|
||||
const diffPercent = diffPixels / totalPixels;
|
||||
const passed = diffPercent <= threshold;
|
||||
|
||||
// Save diff image if comparison failed
|
||||
if (!passed) {
|
||||
writeFileSync(diffPath, PNG.sync.write(diffImg));
|
||||
}
|
||||
|
||||
return {
|
||||
passed,
|
||||
baselinePath,
|
||||
currentPath,
|
||||
diffPath: passed ? undefined : diffPath,
|
||||
diffPixels,
|
||||
diffPercent,
|
||||
newBaseline: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture a screenshot without comparison (for debugging)
|
||||
* @param page - Playwright page
|
||||
* @param name - Screenshot name
|
||||
*/
|
||||
export async function captureScreenshot(page: Page, name: string): Promise<string> {
|
||||
ensureDirectories();
|
||||
const path = join(CURRENT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path });
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update baseline with current screenshot
|
||||
* @param name - Screenshot name
|
||||
*/
|
||||
export function updateBaseline(name: string): boolean {
|
||||
const currentPath = join(CURRENT_DIR, `${name}.png`);
|
||||
const baselinePath = join(BASELINES_DIR, `${name}.png`);
|
||||
|
||||
if (!existsSync(currentPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentBuffer = readFileSync(currentPath);
|
||||
writeFileSync(baselinePath, currentBuffer);
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user