diff --git a/packages/typespec-vscode/test/extension/common/common-steps.ts b/packages/typespec-vscode/test/extension/common/common-steps.ts index fd4fffb580f..d05e2db33ee 100644 --- a/packages/typespec-vscode/test/extension/common/common-steps.ts +++ b/packages/typespec-vscode/test/extension/common/common-steps.ts @@ -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 }); } } diff --git a/packages/typespec-vscode/test/extension/common/mock-dialogs.ts b/packages/typespec-vscode/test/extension/common/mock-dialogs.ts index e8fd008ea2c..d9950b34573 100644 --- a/packages/typespec-vscode/test/extension/common/mock-dialogs.ts +++ b/packages/typespec-vscode/test/extension/common/mock-dialogs.ts @@ -37,7 +37,7 @@ export function stubDialog( return stubMultipleDialogs(app, [{ method, value }]); } -export function stubMultipleDialogs( +export async function stubMultipleDialogs( app: ElectronApplication, mocks: DialogMethodStubPartial[], ) { @@ -53,23 +53,40 @@ export function stubMultipleDialogs( }); // 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)); + } + } } /** diff --git a/packages/typespec-vscode/test/extension/common/utils.ts b/packages/typespec-vscode/test/extension/common/utils.ts index 1624d8332bd..edd66d6ef9e 100644 --- a/packages/typespec-vscode/test/extension/common/utils.ts +++ b/packages/typespec-vscode/test/extension/common/utils.ts @@ -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 diff --git a/packages/typespec-vscode/test/extension/create-typespec.test.ts b/packages/typespec-vscode/test/extension/create-typespec.test.ts index cdbc7afefe5..c08dbefe414 100644 --- a/packages/typespec-vscode/test/extension/create-typespec.test.ts +++ b/packages/typespec-vscode/test/extension/create-typespec.test.ts @@ -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"); @@ -86,6 +89,5 @@ describe.each(CreateCasesConfigList)("CreateTypespecProject", async (item) => { app, ); await expectFilesInDir(expectedResults, workspacePath); - app.close(); }); }); diff --git a/packages/typespec-vscode/test/extension/emit-typespec.test.ts b/packages/typespec-vscode/test/extension/emit-typespec.test.ts index 5f4e9ad2ae4..6cffcf0d599 100644 --- a/packages/typespec-vscode/test/extension/emit-typespec.test.ts +++ b/packages/typespec-vscode/test/extension/emit-typespec.test.ts @@ -104,6 +104,5 @@ describe.each(EmitCasesConfigList)("EmitTypespecProject", async (item) => { } const resultFilePath = path.resolve(workspacePath, "./tsp-output/@typespec"); await expectFilesInDir(expectedResults, resultFilePath); - app.close(); }); }); diff --git a/packages/typespec-vscode/test/extension/import-typespec.test.ts b/packages/typespec-vscode/test/extension/import-typespec.test.ts index 3e4ea9a186d..1bc2cb2e653 100644 --- a/packages/typespec-vscode/test/extension/import-typespec.test.ts +++ b/packages/typespec-vscode/test/extension/import-typespec.test.ts @@ -99,6 +99,5 @@ describe.each(ImportCasesConfigList)("ImportTypespecFromOpenApi3", async (item) ); const resultFilePath = path.resolve(workspacePath, "./ImportTypespecProjectEmptyFolder"); await expectFilesInDir(expectedResults, resultFilePath); - app.close(); }); }); diff --git a/packages/typespec-vscode/test/extension/preview-typespec.test.ts b/packages/typespec-vscode/test/extension/preview-typespec.test.ts index 1e11cf61ace..587af2315d8 100644 --- a/packages/typespec-vscode/test/extension/preview-typespec.test.ts +++ b/packages/typespec-vscode/test/extension/preview-typespec.test.ts @@ -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(); @@ -80,6 +80,5 @@ describe.each(PreviewCasesConfigList)("PreviewAPIDocument", async (item) => { cs, ); await rm(cs.caseDir, { recursive: true }); - app.close(); }); });