Skip to content

Commit 7199022

Browse files
authored
feat: pause on --debug (#38345)
1 parent be32ea9 commit 7199022

File tree

14 files changed

+317
-50
lines changed

14 files changed

+317
-50
lines changed

packages/playwright/src/common/ipc.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,15 @@ export type AttachmentPayload = {
8484
stepId?: string;
8585
};
8686

87+
export type TestErrorsPayload = {
88+
testId: string;
89+
errors: TestInfoErrorImpl[];
90+
};
91+
8792
export type TestInfoErrorImpl = TestInfoError;
8893

8994
export type TestPausedPayload = {
9095
testId: string;
91-
errors: TestInfoErrorImpl[];
9296
};
9397

9498
export type CustomMessageRequestPayload = {
@@ -105,7 +109,6 @@ export type TestEndPayload = {
105109
testId: string;
106110
duration: number;
107111
status: TestStatus;
108-
errors: TestInfoErrorImpl[];
109112
hasNonRetriableError: boolean;
110113
expectedStatus: TestStatus;
111114
annotations: { type: string, description?: string }[];

packages/playwright/src/isomorphic/teleReceiver.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ export type JsonTestResultEnd = {
9696
id: string;
9797
duration: number;
9898
status: reporterTypes.TestStatus;
99-
errors: reporterTypes.TestError[];
99+
/** No longer emitted, but kept for backwards compatibility */
100+
errors?: reporterTypes.TestError[];
100101
/** No longer emitted, but kept for backwards compatibility */
101102
attachments?: JsonAttachment[];
102103
annotations?: TestAnnotation[];
@@ -132,7 +133,7 @@ export type JsonFullResult = {
132133
};
133134

134135
export type JsonEvent = JsonOnConfigureEvent | JsonOnBlobReportMetadataEvent | JsonOnEndEvent | JsonOnExitEvent | JsonOnProjectEvent | JsonOnBeginEvent | JsonOnTestBeginEvent
135-
| JsonOnTestEndEvent | JsonOnStepBeginEvent | JsonOnStepEndEvent | JsonOnAttachEvent | JsonOnErrorEvent | JsonOnStdIOEvent;
136+
| JsonOnTestEndEvent | JsonOnStepBeginEvent | JsonOnStepEndEvent | JsonOnAttachEvent | JsonOnTestErrorEvent | JsonOnErrorEvent | JsonOnStdIOEvent;
136137

137138
export type JsonOnConfigureEvent = {
138139
method: 'onConfigure';
@@ -198,6 +199,15 @@ export type JsonOnAttachEvent = {
198199
params: JsonTestResultOnAttach;
199200
};
200201

202+
export type JsonOnTestErrorEvent = {
203+
method: 'onTestError';
204+
params: {
205+
testId: string;
206+
resultId: string;
207+
error: reporterTypes.TestError;
208+
}
209+
};
210+
201211
export type JsonOnErrorEvent = {
202212
method: 'onError';
203213
params: {
@@ -294,6 +304,10 @@ export class TeleReporterReceiver {
294304
this._onAttach(params.testId, params.resultId, params.attachments);
295305
return;
296306
}
307+
if (method === 'onTestError') {
308+
this._onTestError(params.testId, params.resultId, params.error);
309+
return;
310+
}
297311
if (method === 'onStepEnd') {
298312
this._onStepEnd(params.testId, params.resultId, params.step);
299313
return;
@@ -353,8 +367,11 @@ export class TeleReporterReceiver {
353367
const result = test.results.find(r => r._id === payload.id)!;
354368
result.duration = payload.duration;
355369
result.status = payload.status;
356-
result.errors = payload.errors;
357-
result.error = result.errors?.[0];
370+
// Errors are only present here from legacy blobs. These override all _onTestError events
371+
if (!!payload.errors) {
372+
result.errors = payload.errors;
373+
result.error = result.errors[0];
374+
}
358375
// Attachments are only present here from legacy blobs. These override all _onAttach events
359376
if (!!payload.attachments)
360377
result.attachments = this._parseAttachments(payload.attachments);
@@ -404,6 +421,13 @@ export class TeleReporterReceiver {
404421
})));
405422
}
406423

424+
private _onTestError(testId: string, resultId: string, error: reporterTypes.TestError) {
425+
const test = this._tests.get(testId)!;
426+
const result = test.results.find(r => r._id === resultId)!;
427+
result.errors.push(error);
428+
result.error = result.errors[0];
429+
}
430+
407431
private _onError(error: reporterTypes.TestError) {
408432
this._reporter.onError?.(error);
409433
}

packages/playwright/src/reporters/internalReporter.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,12 @@ export class InternalReporter implements ReporterV2 {
6767
this._reporter.onStdErr?.(chunk, test, result);
6868
}
6969

70+
onTestError(test: TestCase, result: TestResult, error: TestError): void {
71+
addLocationAndSnippetToError(this._config, error, test.location.file);
72+
this._reporter.onTestError?.(test, result, error);
73+
}
74+
7075
onTestEnd(test: TestCase, result: TestResult) {
71-
this._addSnippetToTestErrors(test, result);
7276
this._reporter.onTestEnd?.(test, result);
7377
}
7478

@@ -89,7 +93,7 @@ export class InternalReporter implements ReporterV2 {
8993
}
9094

9195
onError(error: TestError) {
92-
addLocationAndSnippetToError(this._config, error);
96+
addLocationAndSnippetToError(this._config, error, undefined);
9397
this._reporter.onError?.(error);
9498
}
9599

@@ -98,26 +102,17 @@ export class InternalReporter implements ReporterV2 {
98102
}
99103

100104
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
101-
this._addSnippetToStepError(test, step);
105+
if (step.error)
106+
addLocationAndSnippetToError(this._config, step.error, test.location.file);
102107
this._reporter.onStepEnd?.(test, result, step);
103108
}
104109

105110
printsToStdio() {
106111
return this._reporter.printsToStdio ? this._reporter.printsToStdio() : true;
107112
}
108-
109-
private _addSnippetToTestErrors(test: TestCase, result: TestResult) {
110-
for (const error of result.errors)
111-
addLocationAndSnippetToError(this._config, error, test.location.file);
112-
}
113-
114-
private _addSnippetToStepError(test: TestCase, step: TestStep) {
115-
if (step.error)
116-
addLocationAndSnippetToError(this._config, step.error, test.location.file);
117-
}
118113
}
119114

120-
export function addLocationAndSnippetToError(config: FullConfig, error: TestError, file?: string) {
115+
export function addLocationAndSnippetToError(config: FullConfig, error: TestError, file: string | undefined) {
121116
if (error.stack && !error.location)
122117
error.location = prepareErrorStack(error.stack).location;
123118
const location = error.location;

packages/playwright/src/reporters/merge.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ class IdsPatcher {
404404
case 'onProject':
405405
this._onProject(params.project);
406406
return;
407+
case 'onTestError':
407408
case 'onAttach':
408409
case 'onTestBegin':
409410
case 'onStepBegin':
@@ -498,7 +499,7 @@ class PathSeparatorPatcher {
498499
test.annotations?.forEach(annotation => this._updateAnnotationLocation(annotation));
499500
const testResult = jsonEvent.params.result;
500501
testResult.annotations?.forEach(annotation => this._updateAnnotationLocation(annotation));
501-
testResult.errors.forEach(error => this._updateErrorLocations(error));
502+
testResult.errors?.forEach(error => this._updateErrorLocations(error));
502503
(testResult.attachments ?? []).forEach(attachment => {
503504
if (attachment.path)
504505
attachment.path = this._updatePath(attachment.path);
@@ -516,6 +517,10 @@ class PathSeparatorPatcher {
516517
step.annotations?.forEach(annotation => this._updateAnnotationLocation(annotation));
517518
return;
518519
}
520+
if (jsonEvent.method === 'onTestError') {
521+
this._updateErrorLocations(jsonEvent.params.error);
522+
return;
523+
}
519524
if (jsonEvent.method === 'onAttach') {
520525
const attach = jsonEvent.params;
521526
attach.attachments.forEach(attachment => {

packages/playwright/src/reporters/multiplexer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export class Multiplexer implements ReporterV2 {
5454
wrap(() => reporter.onStdErr?.(chunk, test, result));
5555
}
5656

57+
onTestError(test: TestCase, result: TestResult, error: TestError) {
58+
for (const reporter of this._reporters)
59+
wrap(() => reporter.onTestError?.(test, result, error));
60+
}
61+
5762
onTestEnd(test: TestCase, result: TestResult) {
5863
for (const reporter of this._reporters)
5964
wrap(() => reporter.onTestEnd?.(test, result));

packages/playwright/src/reporters/reporterV2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ReporterV2 {
2222
onTestBegin?(test: TestCase, result: TestResult): void;
2323
onStdOut?(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
2424
onStdErr?(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
25+
onTestError?(test: TestCase, result: TestResult, error: TestError): void;
2526
onTestEnd?(test: TestCase, result: TestResult): void;
2627
onEnd?(result: FullResult): Promise<{ status?: FullResult['status'] } | undefined | void> | void;
2728
onExit?(): void | Promise<void>;

packages/playwright/src/reporters/teleEmitter.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ export class TeleReporterEmitter implements ReporterV2 {
7171
});
7272
}
7373

74+
onTestError(test: reporterTypes.TestCase, result: reporterTypes.TestResult, error: reporterTypes.TestError): void {
75+
this._messageSink({
76+
method: 'onTestError',
77+
params: {
78+
testId: test.id,
79+
resultId: (result as any)[this._idSymbol],
80+
error,
81+
}
82+
});
83+
}
84+
7485
onTestEnd(test: reporterTypes.TestCase, result: reporterTypes.TestResult): void {
7586
const testEnd: teleReceiver.JsonTestEnd = {
7687
testId: test.id,
@@ -248,7 +259,6 @@ export class TeleReporterEmitter implements ReporterV2 {
248259
id: (result as any)[this._idSymbol],
249260
duration: result.duration,
250261
status: result.status,
251-
errors: result.errors,
252262
annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : undefined,
253263
};
254264
}

packages/playwright/src/runner/dispatcher.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import type { ProcessExitData } from './processHost';
2828
import type { TestGroup } from './testGroups';
2929
import type { TestError, TestResult, TestStep } from '../../types/testReporter';
3030
import type { FullConfigInternal } from '../common/config';
31-
import type { AttachmentPayload, DonePayload, RunPayload, SerializedConfig, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestOutputPayload, TestPausedPayload } from '../common/ipc';
31+
import type { AttachmentPayload, DonePayload, RunPayload, SerializedConfig, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestErrorsPayload, TestOutputPayload, TestPausedPayload } from '../common/ipc';
3232
import type { Suite } from '../common/test';
3333
import type { TestCase } from '../common/test';
3434
import type { ReporterV2 } from '../reporters/reporterV2';
@@ -322,7 +322,6 @@ class JobDispatcher {
322322
// Do not show more than one error to avoid confusion, but report
323323
// as interrupted to indicate that we did actually start the test.
324324
params.status = 'interrupted';
325-
params.errors = [];
326325
}
327326
const data = this._dataByTestId.get(params.testId);
328327
if (!data) {
@@ -333,8 +332,6 @@ class JobDispatcher {
333332
this._remainingByTestId.delete(params.testId);
334333
const { result, test } = data;
335334
result.duration = params.duration;
336-
result.errors = params.errors;
337-
result.error = result.errors[0];
338335
result.status = params.status;
339336
result.annotations = params.annotations;
340337
test.annotations = [...params.annotations]; // last test result wins
@@ -429,6 +426,23 @@ class JobDispatcher {
429426
}
430427
}
431428

429+
private _onTestErrors(params: TestErrorsPayload) {
430+
if (this._failureTracker.hasReachedMaxFailures()) {
431+
// Do not show more than one error to avoid confusion.
432+
return;
433+
}
434+
435+
const data = this._dataByTestId.get(params.testId)!;
436+
if (!data)
437+
return;
438+
const { test, result } = data;
439+
for (const error of params.errors) {
440+
result.errors.push(error);
441+
result.error = result.errors[0];
442+
this._reporter.onTestError?.(test, result, error);
443+
}
444+
}
445+
432446
private _failTestWithErrors(test: TestCase, errors: TestError[]) {
433447
const runData = this._dataByTestId.get(test.id);
434448
// There might be a single test that has started but has not finished yet.
@@ -439,8 +453,11 @@ class JobDispatcher {
439453
result = test._appendTestResult();
440454
this._reporter.onTestBegin?.(test, result);
441455
}
442-
result.errors = [...errors];
443-
result.error = result.errors[0];
456+
for (const error of errors) {
457+
result.errors.push(error);
458+
result.error = result.errors[0];
459+
this._reporter.onTestError?.(test, result, error);
460+
}
444461
result.status = errors.length ? 'failed' : 'skipped';
445462
this._reportTestEnd(test, result);
446463
this._failedTests.add(test);
@@ -578,31 +595,36 @@ class JobDispatcher {
578595
eventsHelper.addEventListener(worker, 'stepBegin', this._onStepBegin.bind(this)),
579596
eventsHelper.addEventListener(worker, 'stepEnd', this._onStepEnd.bind(this)),
580597
eventsHelper.addEventListener(worker, 'attach', this._onAttach.bind(this)),
598+
eventsHelper.addEventListener(worker, 'testErrors', this._onTestErrors.bind(this)),
581599
eventsHelper.addEventListener(worker, 'testPaused', this._onTestPaused.bind(this, worker)),
582600
eventsHelper.addEventListener(worker, 'done', this._onDone.bind(this)),
583601
eventsHelper.addEventListener(worker, 'exit', this.onExit.bind(this)),
584602
];
585603
}
586604

587605
private _onTestPaused(worker: WorkerHost, params: TestPausedPayload) {
606+
const data = this._dataByTestId.get(params.testId);
607+
if (!data)
608+
return;
609+
610+
const { test, result } = data;
611+
588612
const sendMessage = async (message: { request: any }) => {
589613
try {
590614
if (this.jobResult.isDone())
591615
throw new Error('Test has already stopped');
592616
const response = await worker.sendCustomMessage({ testId: params.testId, request: message.request });
593617
if (response.error)
594-
addLocationAndSnippetToError(this._config.config, response.error);
618+
addLocationAndSnippetToError(this._config.config, response.error, test.location.file);
595619
return response;
596620
} catch (e) {
597621
const error = serializeError(e);
598-
addLocationAndSnippetToError(this._config.config, error);
622+
addLocationAndSnippetToError(this._config.config, error, test.location.file);
599623
return { response: undefined, error };
600624
}
601625
};
602626

603-
for (const error of params.errors)
604-
addLocationAndSnippetToError(this._config.config, error);
605-
this._failureTracker.onTestPaused?.({ ...params, sendMessage });
627+
this._failureTracker.onTestPaused?.({ errors: result.errors, sendMessage });
606628
}
607629

608630
skipWholeJob(): boolean {

packages/playwright/src/runner/testRunner.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,9 @@ export async function runAllTestsWithConfig(config: FullConfigInternal): Promise
479479
createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true }),
480480
...createRunTestsTasks(config),
481481
];
482-
const status = await runTasks(new TestRun(config, reporter), tasks, config.config.globalTimeout);
482+
483+
const testRun = new TestRun(config, reporter, { pauseAtEnd: config.configCLIOverrides.debug, pauseOnError: config.configCLIOverrides.debug });
484+
const status = await runTasks(testRun, tasks, config.config.globalTimeout);
483485

484486
// Calling process.exit() might truncate large stdout/stderr output.
485487
// See https://github.com/nodejs/node/issues/6456.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import path from 'path';
18+
import { traverse, babelParse, T, types as t } from './babelBundle';
19+
import type { Location } from '../../types/testReporter';
20+
21+
function containsLocation(range: T.SourceLocation, location: Location): boolean {
22+
if (location.line < range.start.line || location.line > range.end.line)
23+
return false;
24+
if (location.line === range.start.line && location.column < range.start.column)
25+
return false;
26+
if (location.line === range.end.line && location.column > range.end.column)
27+
return false;
28+
return true;
29+
}
30+
31+
export function findTestEndLocation(text: string, testStartLocation: Location): Location | undefined {
32+
const ast = babelParse(text, path.basename(testStartLocation.file), false);
33+
let result: Location | undefined;
34+
traverse(ast, {
35+
enter(path) {
36+
if (t.isCallExpression(path.node) && path.node.loc && containsLocation(path.node.loc, testStartLocation)) {
37+
const callNode = path.node;
38+
const funcNode = callNode.arguments[callNode.arguments.length - 1];
39+
if (callNode.arguments.length >= 2 && t.isFunction(funcNode) && funcNode.body.loc)
40+
result = { file: testStartLocation.file, ...funcNode.body.loc.end };
41+
}
42+
}
43+
});
44+
return result;
45+
}

0 commit comments

Comments
 (0)