Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export async function preContrastResult(
await page.waitForSelector(`:text("${text}")`, { timeout });
} catch (e) {
await cs.screenshot(page, "error");
app.close();
throw new Error(`${errorMessage} - Timed out waiting for text: "${text}" - ${e}`, { cause: e });
}
}
Expand Down
51 changes: 34 additions & 17 deletions packages/typespec-vscode/test/extension/common/mock-dialogs.ts
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this will solve both issue if they are not the same thing underneth. The 2nd one crash before any of the code changes are

packages/typespec-vscode test:e2e:  FAIL  create-typespec.test.ts > CreateTypespecProject > CreateTypespecProject GenericRestAPI Trigger CommandPalette EmptyFolder
packages/typespec-vscode test:e2e: Error: electronApplication.firstWindow: Target page, context or browser has been closed

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the feedback! You're right that these are two different errors with different root causes.

  1. Error (Execution context was destroyed at mock-dialogs.ts:56) — Fixed by adding retry logic in stubMultipleDialogs and waiting for VS Code UI readiness (waitForLoadState + waitForSelector) before mocking dialogs.

  2. Error (Target page, context or browser has been closed at firstWindow()) — I think this is caused by the old app.close() calls not being awaited. The previous code had bare app.close() (without await) at the end of each test, which means Electron could still be shutting down when the next test launches a new instance — causing a race condition that makes firstWindow() fail. I've moved cleanup into the teardown with await app.close() to ensure each Electron process is fully closed before the next test starts.

If this turns out to be insufficient (e.g., Electron crashes on startup for other reasons in CI), I can add retry logic around firstWindow() as well. Let me know what you think.

Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function stubDialog<T extends keyof Dialog>(
return stubMultipleDialogs(app, [{ method, value }]);
}

export function stubMultipleDialogs<T extends keyof Dialog>(
export async function stubMultipleDialogs<T extends keyof Dialog>(
app: ElectronApplication,
mocks: DialogMethodStubPartial<T>[],
) {
Expand All @@ -53,23 +53,40 @@ export function stubMultipleDialogs<T extends keyof Dialog>(
});

// https://github.com/microsoft/playwright/issues/8278#issuecomment-1009957411
return app.evaluate(({ dialog }, mocks) => {
mocks.forEach((mock) => {
const thisDialog = dialog[mock.method];
if (!thisDialog) {
throw new Error(`can't find ${mock.method} on dialog module.`);
}
if (mock.method.endsWith("Sync")) {
dialog[mock.method] = () => {
return mock.value;
};
} else {
dialog[mock.method] = async () => {
return mock.value;
};
// Retry to handle transient "Execution context was destroyed" errors
// that occur when VS Code reloads during startup.
const maxRetries = 3;
const retryDelayMs = 1000;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await app.evaluate(({ dialog }, mocks) => {
mocks.forEach((mock) => {
const thisDialog = dialog[mock.method];
if (!thisDialog) {
throw new Error(`can't find ${mock.method} on dialog module.`);
}
if (mock.method.endsWith("Sync")) {
dialog[mock.method] = () => {
return mock.value;
};
} else {
dialog[mock.method] = async () => {
return mock.value;
};
}
});
}, mocksRequired);
} catch (error: any) {
const isRetryable =
error.message?.includes("Execution context was destroyed") ||
error.message?.includes("Target page, context or browser has been closed");
if (!isRetryable || attempt === maxRetries) {
throw error;
}
});
}, mocksRequired);
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
}
}
}

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/typespec-vscode/test/extension/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,18 @@ export const test = baseTest.extend<{
`--folder-uri=file:${path.resolve(workspacePath)}`,
].filter((v): v is string => !!v),
});

// Ensure Electron is always closed on teardown, even if later steps fail.
teardowns.push(async () => {
try {
await app.close();
} catch (error) {}
});

const page = await app.firstWindow();
// Wait for the page to fully load to reduce the chance of
// VS Code reloading the window and destroying the execution context.
await page.waitForLoadState("domcontentloaded");
const tracePath = join(projectRoot, "test-results", task.name, "trace.zip");
const artifactsDir = join(tempDir, "playwright-artifacts");
await fs.promises.mkdir(artifactsDir, { recursive: true }); // make sure the directory exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ describe.each(CreateCasesConfigList)("CreateTypespecProject", async (item) => {
workspacePath: workspacePath,
});
await cs.screenshot(page, "after_launch");
// Wait for VS Code UI to be ready before mocking dialogs to avoid
// "Execution context was destroyed" errors from window reloads.
await page.waitForSelector(".explorer-viewlet", { timeout: 30000 });
await mockShowOpenDialog(app, [workspacePath]);
await startWithCommandPalette(page, "Create Typespec Project", cs);
await cs.screenshot(page, "after_start_command");
Expand All @@ -86,6 +89,5 @@ describe.each(CreateCasesConfigList)("CreateTypespecProject", async (item) => {
app,
);
await expectFilesInDir(expectedResults, workspacePath);
app.close();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,5 @@ describe.each(EmitCasesConfigList)("EmitTypespecProject", async (item) => {
}
const resultFilePath = path.resolve(workspacePath, "./tsp-output/@typespec");
await expectFilesInDir(expectedResults, resultFilePath);
app.close();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,5 @@ describe.each(ImportCasesConfigList)("ImportTypespecFromOpenApi3", async (item)
);
const resultFilePath = path.resolve(workspacePath, "./ImportTypespecProjectEmptyFolder");
await expectFilesInDir(expectedResults, resultFilePath);
app.close();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe.each(PreviewCasesConfigList)("PreviewAPIDocument", async (item) => {
test(caseName, async ({ launch }) => {
const cs = new CaseScreenshot(caseName);
const workspacePath = PreviewTypespecProjectFolderPath;
const { page, app } = await launch({
const { page } = await launch({
workspacePath,
});
await page.getByRole("treeitem", { name: "main.tsp" }).locator("a").click();
Expand All @@ -80,6 +80,5 @@ describe.each(PreviewCasesConfigList)("PreviewAPIDocument", async (item) => {
cs,
);
await rm(cs.caseDir, { recursive: true });
app.close();
});
});
Loading