Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f79c29c
update to playwright 1.57.0
shouples Dec 30, 2025
5e3919c
handle double-array structure for plugins in webview tests
shouples Dec 30, 2025
e8e5c3b
set up manual tracing to fix trace CSS/font issues for snapshot
shouples Dec 30, 2025
77a89f5
always stop tracing
shouples Dec 30, 2025
0089474
add withTimeout helper; make sure saveVSCodeWindowLogs doesn't hang
shouples Dec 30, 2025
d7558aa
make `npx playwright test` flags more explicit since `-gv` doesn't ex…
shouples Dec 30, 2025
1790952
add teardown logs to debug timeouts
shouples Jan 2, 2026
a382142
revert teardown logging, check child processes
shouples Jan 2, 2026
615cd9f
allow status 1 when `output` exists
shouples Jan 2, 2026
58d1ca5
add logging for active resources; attempt to sigkill process group
shouples Jan 2, 2026
8ea5fe9
remove listChildResources; add clearTimeout for withTimeout cleanup
shouples Jan 2, 2026
754232f
downgrade to playwright 1.56.1 to see if 1.57.0 introduced worker tea…
shouples Jan 5, 2026
769917d
downgrade to playwright 1.55.1
shouples Jan 5, 2026
f3b0588
back to playwright 1.57.0
shouples Jan 5, 2026
2519c8c
claude: extra debugging and attempting to unref lingering handles and…
shouples Jan 5, 2026
8341c13
add maxFailures placeholder
shouples Jan 6, 2026
818a890
revert worker debug fixture and perma-tracing
shouples Jan 6, 2026
885cf2a
push electron shutdown into standalone function
shouples Jan 6, 2026
5e4c831
revert VS Code log saving step using obsolete function
shouples Jan 6, 2026
249cf9a
set up standalone shutdownElectronApp helper function
shouples Jan 6, 2026
9719e8d
capture screenshot of the CCloud auth page and add to testInfo for an…
shouples Jan 6, 2026
d99f062
use testInfo for any setupCCloudConnection callers
shouples Jan 6, 2026
68d2bda
minor fix for spacing
shouples Jan 6, 2026
26ae735
remove temporary forced failure
shouples Jan 6, 2026
b1c2aab
only capture a screenshot for an assertion error, not every time the …
shouples Jan 6, 2026
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
4 changes: 2 additions & 2 deletions Gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -902,8 +902,8 @@ export function e2eRun(done) {
"-c",
"tests/e2e/playwright.config.ts",
"tests/e2e",
...(testFilter ? ["-g", testFilter] : []),
...(testExcludeFilter ? ["-gv", testExcludeFilter] : []),
...(testFilter ? ["--grep", testFilter] : []),
...(testExcludeFilter ? ["--grep-invert", testExcludeFilter] : []),
Comment on lines +905 to +906
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No more -gv:

> npx playwright test --help
...
-g, --grep <grep>                Only run tests matching this regular expression (default: ".*")
  --global-timeout <timeout>       Maximum time this test suite can run in milliseconds (default: unlimited)
  --grep-invert <grep>             Only run tests that do not match this regular expression
...

];
console.log("Running command: npx", command.join(" "));

Expand Down
28 changes: 16 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2540,7 +2540,7 @@
"@0no-co/graphqlsp": "^1.12.8",
"@eslint/js": "^9.4.0",
"@openapitools/openapi-generator-cli": "^2.22.0",
"@playwright/test": "^1.45.0",
"@playwright/test": "^1.57.0",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-json": "^6.1.0",
Expand Down
14 changes: 9 additions & 5 deletions src/webview/bindings/bindings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import esbuild from "rollup-plugin-esbuild";
import { test } from "../baseTest";

test.use({
// @ts-expect-error: Playwright 1.46+ requires double-array for plugins,
// see https://github.com/microsoft/playwright/releases/tag/v1.46.0
plugins: [
esbuild({ jsx: "automatic", target: "es2022", exclude: [/node_modules/] }),
replace({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
preventAssignment: true,
}),
[
esbuild({ jsx: "automatic", target: "es2022", exclude: [/node_modules/] }),
replace({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
preventAssignment: true,
}),
],
],
});

Expand Down
14 changes: 9 additions & 5 deletions src/webview/bindings/custom-elements.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import esbuild from "rollup-plugin-esbuild";
import { test } from "../baseTest";

test.use({
// @ts-expect-error: Playwright 1.46+ requires double-array for plugins,
// see https://github.com/microsoft/playwright/releases/tag/v1.46.0
plugins: [
esbuild({ jsx: "automatic", target: "es2022", exclude: [/node_modules/] }),
replace({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
preventAssignment: true,
}),
[
esbuild({ jsx: "automatic", target: "es2022", exclude: [/node_modules/] }),
replace({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
preventAssignment: true,
}),
],
],
});

Expand Down
16 changes: 10 additions & 6 deletions src/webview/direct-connect-form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,20 @@ function stylesheet(options: any = {}): Plugin {
}
test.use({
template: render(template, { nonce: "testing" }),
// @ts-expect-error: Playwright 1.46+ requires double-array for plugins,
// see https://github.com/microsoft/playwright/releases/tag/v1.46.0
plugins: [
virtual({
comms: `
[
virtual({
comms: `
import * as sinon from 'sinon';
export const sendWebviewMessage = sinon.stub();
`,
}),
alias({ entries: { "./comms/comms": "comms" } }),
stylesheet({ include: ["**/*.css"], minify: false }),
esbuild({ jsx: "automatic", target: "es2022", exclude: [/node_modules/] }),
}),
alias({ entries: { "./comms/comms": "comms" } }),
stylesheet({ include: ["**/*.css"], minify: false }),
esbuild({ jsx: "automatic", target: "es2022", exclude: [/node_modules/] }),
],
],
});
test("renders form html correctly", async ({ page }) => {
Expand Down
16 changes: 10 additions & 6 deletions src/webview/scaffold-form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,20 @@ function stylesheet(options: any = {}): Plugin {

test.use({
template: render(template, { nonce: "testing" }),
// @ts-expect-error: Playwright 1.46+ requires double-array for plugins,
// see https://github.com/microsoft/playwright/releases/tag/v1.46.0
plugins: [
virtual({
comms: `
[
virtual({
comms: `
import * as sinon from 'sinon';
export const sendWebviewMessage = sinon.stub();
`,
}),
alias({ entries: { "./comms/comms": "comms" } }),
stylesheet({ include: ["**/*.css"], minify: false }),
esbuild({ jsx: "automatic", target: "es2022", exclude: [/node_modules/] }),
}),
alias({ entries: { "./comms/comms": "comms" } }),
stylesheet({ include: ["**/*.css"], minify: false }),
esbuild({ jsx: "automatic", target: "es2022", exclude: [/node_modules/] }),
],
],
});

Expand Down
160 changes: 110 additions & 50 deletions tests/e2e/baseTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const test = testBase.extend<VSCodeFixtures>({
await use(tempDir);
},

electronApp: async ({ trace, testTempDir }, use, testInfo) => {
electronApp: async ({ testTempDir }, use, testInfo) => {
const testConfigs = getTestSetupCache();

// launch VS Code with Electron using args pattern from vscode-test
Expand All @@ -106,67 +106,47 @@ export const test = testBase.extend<VSCodeFixtures>({
`--extensionDevelopmentPath=${testConfigs.outPath}`,
],
});

if (!electronApp) {
throw new Error("Failed to launch VS Code electron app");
}

// wait for VS Code to be ready before trying to stub dialogs
const page = await electronApp.firstWindow();
if (!page) {
// usually this means the launch args were incorrect and/or the app didn't start correctly
throw new Error("Failed to get first window from VS Code");
}
await page.waitForLoadState("domcontentloaded");
await page.locator(".monaco-workbench").waitFor({ timeout: 30000 });

// Stub all dialogs by default; tests can still override as needed.
// For available `method` values to use with `stubMultipleDialogs`, see:
// https://www.electronjs.org/docs/latest/api/dialog
await stubAllDialogs(electronApp);

// on*, retain-on*
if (trace.toString().includes("on")) {
await electronApp.context().tracing.start({
screenshots: true,
snapshots: true,
sources: true,
title: `${process.platform} ${process.arch}: ${testInfo.title} (${testInfo.tags.join(", ")})`,
});
}

await use(electronApp);
const context = electronApp.context();
// always start tracing manually, but decide later whether to save it based on test result
await context.tracing.start({
screenshots: true,
snapshots: true,
sources: true,
title: `${process.platform} ${process.arch}: ${testInfo.title} (${testInfo.tags.join(", ")})`,
});

try {
// shorten grace period for shutdown to avoid hanging the entire test run, but don't SIGKILL
// early because we might lose trace/screenshot/snapshot data
await Promise.race([
electronApp.close(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("electronApp.close() timeout after 5s")), 5_000),
),
]);
} catch {
console.warn("Timed out waiting for Electron to close, killing process...");
try {
electronApp.process().kill("SIGKILL");
console.info("Killed Electron process");
} catch (err) {
console.warn(`Error killing Electron process: ${err}`);
// wait for VS Code to be ready before trying to stub dialogs
const page = await electronApp.firstWindow();
await page.waitForLoadState("domcontentloaded");
await page.locator(".monaco-workbench").waitFor({ timeout: 30000 });

// Stub all dialogs by default; tests can still override as needed.
// For available `method` values to use with `stubMultipleDialogs`, see:
// https://www.electronjs.org/docs/latest/api/dialog
await stubAllDialogs(electronApp);

await use(electronApp);
} finally {
// only save and attach the trace for failed tests
if (testInfo.status !== testInfo.expectedStatus) {
const tracePath = path.join(testInfo.outputDir, "trace.zip");
await context.tracing.stop({ path: tracePath });
await testInfo.attach("trace", { path: tracePath, contentType: "application/zip" });
} else {
await context.tracing.stop();
}
}

await shutdownElectronApp(electronApp);
},

page: async ({ electronApp, testTempDir }, use, testInfo) => {
if (!electronApp) {
throw new Error("electronApp is null - failed to launch VS Code");
}

const page = await electronApp.firstWindow();
if (!page) {
// shouldn't happen since we waited for the workbench above
throw new Error("Failed to get first window from VS Code");
}
Comment on lines -161 to -169
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No need for these since electronApp will holler loudly if it can't start or we can't get the first window


await globalBeforeEach(page, electronApp);

Expand Down Expand Up @@ -413,3 +393,83 @@ async function saveVSCodeWindowLogs(testTempDir: string, testInfo: TestInfo): Pr
console.error("Error zipping VS Code logs directory:", error);
}
}

/** Partial type for handles that may have an unref() method. */
type PartialHandle = { unref?: () => void };

/**
* Extended type for process with the (deprecated) `_getActiveHandles` method.
*
* NOTE: https://nodejs.org/api/deprecations.html#DEP0161 suggests using
* `process.getActiveResourcesInfo()`, but that only provides the names of active resources,
* not the actual handles to unref and allow the process to exit cleanly.
*/
type NodeProcessWithGetActiveHandles = NodeJS.Process & {
_getActiveHandles?: () => PartialHandle[];
};

/**
* Shut down the Electron app and clean up any lingering handles/requests so the Playwright worker
* can exit cleanly.
*
* With Playwright v1.50.0+, the behavior of `electronApp.close()` changes so it waits for the
* Electron process to exit, but if there are any lingering handles or requests, that may never
* happen, causing the test worker to hang and eventually time out.
Comment on lines +417 to +419
Copy link
Contributor Author

Choose a reason for hiding this comment

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

microsoft/playwright#29431

Wait until the process has exited before returning from ElectronApplication.close() (like for a normal browser - Browser.close() -> gracefullyClose).

*
* This function attempts to close the app gracefully, but if it doesn't exit within a timeout,
* it force kills the process group. It also unrefs any remaining active handles to allow the
* worker teardown to complete.
*/
async function shutdownElectronApp(electronApp: ElectronApplication): Promise<void> {
// 1) wait for close() to complete, but with a short timeout
let timeout: NodeJS.Timeout | undefined;
try {
await Promise.race([
electronApp.close(),
new Promise((resolve) => {
timeout = setTimeout(resolve, 1000);
}),
]);
} catch {
// no need to deal with errors when we're trying to shut everything down here
} finally {
if (timeout !== undefined) {
clearTimeout(timeout);
}
}

// 2) check if the Electron process is still running, and force kill if so
try {
const proc = electronApp.process();
const pid = proc?.pid;
if (pid && pid > 1) {
process.kill(pid, 0); // status check
console.warn("Electron still running after close(), force killing process group...");
process.kill(-pid, 9); // SIGKILL entire process group (negative PID)
}
} catch {
// process no longer exists, no action needed
}

// 3) unref any remaining handles to allow the worker teardown to complete cleanly
try {
const handles = (process as NodeProcessWithGetActiveHandles)._getActiveHandles?.() || [];
let unrefdHandles = 0;
handles.forEach((handle: PartialHandle) => {
if (handle && typeof handle.unref === "function") {
try {
// see description for https://nodejs.org/api/process.html#processunrefmayberefable
handle.unref();
unrefdHandles++;
} catch {
// some handles may not support unref, not much we can do
}
}
});
if (handles.length) {
console.debug(`Unreferenced ${unrefdHandles}/${handles.length} handle(s)`);
}
} catch {
// ignore errors during unref since we can't do much about them
}
}
3 changes: 2 additions & 1 deletion tests/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const WINDOWS_FACTOR = process.platform === "win32" ? 2 : 1;
export default defineConfig({
testDir: path.normalize(path.join(__dirname, "specs")),
forbidOnly: !!process.env.CI,
// maxFailures: 1, // uncomment for local dev/debugging purposes
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not strictly necessary for this PR, but helpful to have as a reference

retries: 2,
timeout: 120000,
workers: 1,
Expand All @@ -42,7 +43,7 @@ export default defineConfig({
]
: [["list"], ["html"]],
use: {
trace: "retain-on-failure",
trace: "off", // manually configured in baseTest.ts
screenshot: "only-on-failure",
video: "retain-on-failure",
},
Expand Down