Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 80 additions & 5 deletions packages/web-integration/src/playwright/ai-fixture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { writeFileSync } from 'node:fs';
import { rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { PlaywrightAgent, type PlaywrightWebPage } from '@/playwright/index';
Expand Down Expand Up @@ -47,6 +47,45 @@ const groupAndCaseForTest = (testInfo: TestInfo) => {
const midsceneAgentKeyId = '_midsceneAgentId';
export const midsceneDumpAnnotationId = 'MIDSCENE_DUMP_ANNOTATION';

// Global tracking of temporary dump files for cleanup
const globalTempFiles = new Set<string>();
let cleanupHandlersRegistered = false;
let cleanupComplete = false;

// Register process exit handlers to clean up temp files
function registerCleanupHandlers() {
if (cleanupHandlersRegistered) return;
cleanupHandlersRegistered = true;

const cleanup = () => {
// Prevent duplicate cleanup if already run
if (cleanupComplete) return;
cleanupComplete = true;

debugPage(`Cleaning up ${globalTempFiles.size} temporary dump files`);

// Convert Set to array to avoid iteration issues while deleting
const filesToClean = Array.from(globalTempFiles);
for (const filePath of filesToClean) {
try {
rmSync(filePath, { force: true });
} catch (error) {
// Silently ignore errors during cleanup
debugPage(`Failed to clean up temp file: ${filePath}`, error);
}
}

// Clear the Set after all files are processed
globalTempFiles.clear();
};

// Register cleanup on process exit
process.once('SIGINT', cleanup);
process.once('SIGTERM', cleanup);
process.once('exit', cleanup);
process.once('beforeExit', cleanup);
Comment on lines +83 to +86
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using process.once() for multiple event handlers creates a race condition. If cleanup runs on 'SIGINT', the 'exit' handler will still fire and attempt to clean already-deleted files. Use a flag to track if cleanup has already run, or consolidate to a single exit handler that covers all scenarios.

Copilot uses AI. Check for mistakes.
}

type PlaywrightCacheConfig = {
strategy?: 'read-only' | 'read-write' | 'write-only';
id?: string;
Expand Down Expand Up @@ -75,6 +114,12 @@ export const PlaywrightAiFixture = (options?: {
return processCacheConfig(cache as Cache, id);
};

// Track temporary dump files for each page
const pageTempFiles = new Map<string, string>(); // pageId -> tempFilePath

// Register global cleanup handlers
registerCleanupHandlers();

const pageAgentMap: Record<string, PageAgent<PlaywrightWebPage>> = {};
const createOrReuseAgentForPage = (
page: OriginPlaywrightPage,
Expand All @@ -100,11 +145,20 @@ export const PlaywrightAiFixture = (options?: {
});

pageAgentMap[idForPage].onDumpUpdate = (dump: string) => {
updateDumpAnnotation(testInfo, dump);
updateDumpAnnotation(testInfo, dump, idForPage);
};

page.on('close', () => {
debugPage('page closed');

// Note: We don't clean up temp files here because the reporter
// needs to read them in onTestEnd. The reporter will clean them up
// after reading. If the test is interrupted (Ctrl+C), the process
// exit handlers will clean up remaining temp files.

// However, we do clean up the pageTempFiles Map entry to avoid memory leaks
pageTempFiles.delete(idForPage);

pageAgentMap[idForPage].destroy();
delete pageAgentMap[idForPage];
});
Expand Down Expand Up @@ -180,14 +234,35 @@ export const PlaywrightAiFixture = (options?: {
});
}

const updateDumpAnnotation = (test: TestInfo, dump: string) => {
// Write dump to temporary file
const tempFileName = `midscene-dump-${test.testId || uuid()}-${Date.now()}.json`;
const updateDumpAnnotation = (
test: TestInfo,
dump: string,
pageId: string,
) => {
// 1. First, clean up the old temp file if it exists
const oldTempFilePath = pageTempFiles.get(pageId);
if (oldTempFilePath) {
// Remove old temp file from tracking and try to delete it
globalTempFiles.delete(oldTempFilePath);
try {
rmSync(oldTempFilePath, { force: true });
} catch (error) {
// Silently ignore if old file is already cleaned up
}
}

// 2. Create new temp file with predictable name using pageId
const tempFileName = `midscene-dump-${test.testId || uuid()}-${pageId}.json`;
const tempFilePath = join(tmpdir(), tempFileName);

// 3. Write dump to the new temporary file
writeFileSync(tempFilePath, dump, 'utf-8');
debugPage(`Dump written to temp file: ${tempFilePath}`);

// 4. Track the new temp file
pageTempFiles.set(pageId, tempFilePath);
globalTempFiles.add(tempFilePath);

// Store only the file path in annotation
const currentAnnotation = test.annotations.find((item) => {
return item.type === midsceneDumpAnnotationId;
Expand Down