Skip to content

Commit

Permalink
fix(debug): run global setup before debug (#486)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored May 21, 2024
1 parent 83a4180 commit 4a4b743
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 18 deletions.
8 changes: 1 addition & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,7 @@ export class Extension implements RunHooks {
const result = model.narrowDownLocations(items);
if (!result.testIds && !result.locations)
continue;
let globalSetupResult: reporterTypes.FullResult['status'] = 'passed';
if (model.canRunGlobalHooks('setup')) {
const testListener = this._errorReportingListener(this._testRun, testItemForGlobalErrors);
globalSetupResult = await model.runGlobalHooks('setup', testListener);
}
if (globalSetupResult === 'passed')
await this._runTest(this._testRun, items, testItemForGlobalErrors, new Set(), model, mode === 'debug', enqueuedTests.length === 1);
await this._runTest(this._testRun, items, testItemForGlobalErrors, new Set(), model, mode === 'debug', enqueuedTests.length === 1);
}
} finally {
this._activeSteps.clear();
Expand Down
29 changes: 20 additions & 9 deletions src/playwrightTestServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ export class PlaywrightTestServer {
const testServer = await this._testServer();
if (!testServer)
return 'failed';
return await this._runGlobalHooksInServer(testServer, type, testListener);
}

private async _runGlobalHooksInServer(testServer: TestServerConnection, type: 'setup' | 'teardown', testListener: reporterTypes.ReporterV2): Promise<'passed' | 'failed' | 'interrupted' | 'timedout'> {
const teleReceiver = new TeleReporterReceiver(testListener, {
mergeProjects: true,
mergeTestCases: true,
Expand All @@ -119,8 +122,8 @@ export class PlaywrightTestServer {

try {
if (type === 'setup') {
const { report, status } = await testServer.runGlobalSetup({});
testListener.onStdOut?.('\x1b[2mRunning global setup if any\u2026\x1b[0m\n');
const { report, status } = await testServer.runGlobalSetup({});
for (const message of report)
teleReceiver.dispatch(message);
return status;
Expand Down Expand Up @@ -207,7 +210,7 @@ export class PlaywrightTestServer {

const testDirs = this._model.enabledProjects().map(project => project.project.testDir);

let testServer: TestServerConnection | undefined;
let debugTestServer: TestServerConnection | undefined;
let disposable: vscodeTypes.Disposable | undefined;
try {
await this._vscode.debug.startDebugging(undefined, {
Expand All @@ -233,8 +236,8 @@ export class PlaywrightTestServer {
if (token?.isCancellationRequested)
return;
const address = await addressPromise;
testServer = new TestServerConnection(address);
await testServer.initialize({
debugTestServer = new TestServerConnection(address);
await debugTestServer.initialize({
serializer: require.resolve('./oopReporter'),
closeOnDisconnect: true,
});
Expand All @@ -245,6 +248,12 @@ export class PlaywrightTestServer {
if (!locations && !testIds)
return;

const result = await this._runGlobalHooksInServer(debugTestServer, 'setup', reporter);
if (result !== 'passed')
return;
if (token?.isCancellationRequested)
return;

// Locations are regular expressions.
const locationPatterns = locations ? locations.map(escapeRegex) : undefined;
const options: Parameters<TestServerInterface['runTests']>['0'] = {
Expand All @@ -253,22 +262,24 @@ export class PlaywrightTestServer {
testIds,
...runOptions,
};
testServer.runTests(options);
debugTestServer.runTests(options);

token.onCancellationRequested(() => {
testServer!.stopTestsNoReply({});
debugTestServer!.stopTestsNoReply({});
});
disposable = testServer.onStdio(params => {
disposable = debugTestServer.onStdio(params => {
if (params.type === 'stdout')
reporter.onStdOut?.(unwrapString(params));
if (params.type === 'stderr')
reporter.onStdErr?.(unwrapString(params));
});
const testEndPromise = this._wireTestServer(testServer, reporter, token);
const testEndPromise = this._wireTestServer(debugTestServer, reporter, token);
await testEndPromise;
} finally {
disposable?.dispose();
testServer?.close();
if (!token.isCancellationRequested && debugTestServer && !debugTestServer.isClosed())
await this._runGlobalHooksInServer(debugTestServer, 'teardown', reporter);
debugTestServer?.close();
await this._options.runHooks.onDidRunTests(true);
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/testModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,8 @@ export class TestModel {
}

async runGlobalHooks(type: 'setup' | 'teardown', testListener: reporterTypes.ReporterV2): Promise<reporterTypes.FullResult['status']> {
if (!this.canRunGlobalHooks(type))
return 'passed';
if (type === 'setup') {
if (this._ranGlobalSetup)
return 'passed';
Expand Down Expand Up @@ -465,6 +467,14 @@ export class TestModel {
async runTests(items: vscodeTypes.TestItem[], reporter: reporterTypes.ReporterV2, token: vscodeTypes.CancellationToken) {
if (token?.isCancellationRequested)
return;

// Run global setup with the first test.
let globalSetupResult: reporterTypes.FullResult['status'] = 'passed';
if (this.canRunGlobalHooks('setup'))
globalSetupResult = await this.runGlobalHooks('setup', reporter);
if (globalSetupResult !== 'passed')
return;

const externalOptions = await this._options.runHooks.onWillRunTests(this.config, false);
const showBrowser = this._options.settingsModel.showBrowser.get() && !!externalOptions.connectWsEndpoint;

Expand Down Expand Up @@ -503,6 +513,12 @@ export class TestModel {
async debugTests(items: vscodeTypes.TestItem[], reporter: reporterTypes.ReporterV2, token: vscodeTypes.CancellationToken) {
if (token?.isCancellationRequested)
return;

// Underlying debugTest implementation will run the global setup.
await this.runGlobalHooks('teardown', reporter);
if (token?.isCancellationRequested)
return;

const externalOptions = await this._options.runHooks.onWillRunTests(this.config, true);
const options: PlaywrightTestRunOptions = {
headed: !this._options.isUnderTest,
Expand Down
6 changes: 6 additions & 0 deletions src/upstream/testServerConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
private _ws: WebSocket;
private _callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
private _connectedPromise: Promise<void>;
private _isClosed = false;

constructor(wsURL: string) {
this.onClose = this._onCloseEmitter.event;
Expand Down Expand Up @@ -72,11 +73,16 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
this._ws.addEventListener('error', r);
});
this._ws.addEventListener('close', () => {
this._isClosed = true;
this._onCloseEmitter.fire();
clearInterval(pingInterval);
});
}

isClosed(): boolean {
return this._isClosed;
}

private async _sendMessage(method: string, params?: any): Promise<any> {
const logForTest = (globalThis as any).__logForTest;
logForTest?.({ method, params });
Expand Down
53 changes: 52 additions & 1 deletion tests/debug-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { expect, test, escapedPathSep } from './utils';
import { TestRun, DebugSession } from './mock/vscode';
import { TestRun, DebugSession, stripAnsi } from './mock/vscode';

test('should debug all tests', async ({ activate }) => {
const { vscode } = await activate({
Expand Down Expand Up @@ -56,6 +56,7 @@ test('should debug all tests', async ({ activate }) => {
testIds: undefined
})
},
{ method: 'runGlobalTeardown', params: {} },
]);
});

Expand Down Expand Up @@ -101,6 +102,7 @@ test('should debug one test', async ({ activate }) => {
testIds: [expect.any(String)]
})
},
{ method: 'runGlobalTeardown', params: {} },
]);
});

Expand Down Expand Up @@ -199,3 +201,52 @@ test('should pass all args as string[] when debugging', async ({ activate }) =>
expect(session.configuration.args.filter(arg => typeof arg !== 'string')).toEqual([]);
await onDidTerminateDebugSession;
});

test('should run global setup before debugging', async ({ activate }, testInfo) => {
const { vscode, testController } = await activate({
'playwright.config.js': `module.exports = {
testDir: 'tests',
globalSetup: 'globalSetup.ts',
globalTeardown: 'globalTeardown.ts',
}`,
'globalSetup.ts': `
async function globalSetup(config: FullConfig) {
console.log('RUN GLOBAL SETUP');
process.env.MAGIC_NUMBER = '42';
}
export default globalSetup;
`,
'globalTeardown.ts': `
async function globalTeardown(config: FullConfig) {
console.log('RUN GLOBAL TEARDOWN');
delete process.env.MAGIC_NUMBER;
}
export default globalTeardown;
`,
'tests/test.spec.ts': `
import { test, expect } from '@playwright/test';
test('should pass', async () => {
console.log('MAGIC NUMBER: ' + process.env.MAGIC_NUMBER);
expect(process.env.MAGIC_NUMBER).toBe('42');
});
`
});

const testRunPromise = new Promise<TestRun>(f => testController.onDidCreateTestRun(f));
await testController.expandTestItems(/test.spec/);
const testItems = testController.findTestItems(/pass/);
const profile = testController.debugProfile();
await profile.run(testItems);
const testRun = await testRunPromise;
expect(testRun.renderLog({ messages: true })).toBe(`
tests > test.spec.ts > should pass [2:0]
enqueued
enqueued
started
passed
`);

await expect.poll(() => stripAnsi(vscode.debug.output)).toContain(`RUN GLOBAL SETUP`);
await expect.poll(() => stripAnsi(vscode.debug.output)).toContain(`MAGIC NUMBER: 42`);
await expect.poll(() => stripAnsi(vscode.debug.output)).toContain(`RUN GLOBAL TEARDOWN`);
});
2 changes: 1 addition & 1 deletion tests/mock/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,6 @@ function trimLog(log: string) {
}

const ansiRegex = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g');
function stripAnsi(str: string): string {
export function stripAnsi(str: string): string {
return str.replace(ansiRegex, '');
}

0 comments on commit 4a4b743

Please sign in to comment.