@smoothdeploy/playwright 1.58.1 → 1.58.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +52 -4
- package/lib/reporters/smoothdeploy.js +65 -28
- package/package.json +2 -2
- package/types/test.d.ts +1 -0
package/lib/index.js
CHANGED
|
@@ -76,6 +76,7 @@ const playwrightFixtures = {
|
|
|
76
76
|
}, { scope: "worker", option: true, box: true }],
|
|
77
77
|
screenshot: ["off", { scope: "worker", option: true, box: true }],
|
|
78
78
|
resultScreenshots: [false, { scope: "worker", option: true, box: true }],
|
|
79
|
+
pauseOnFailure: [false, { scope: "worker", option: true, box: true }],
|
|
79
80
|
video: ["off", { scope: "worker", option: true, box: true }],
|
|
80
81
|
trace: ["off", { scope: "worker", option: true, box: true }],
|
|
81
82
|
_browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
|
|
@@ -229,9 +230,9 @@ const playwrightFixtures = {
|
|
|
229
230
|
playwright._defaultContextTimeout = void 0;
|
|
230
231
|
playwright._defaultContextNavigationTimeout = void 0;
|
|
231
232
|
}, { auto: "all-hooks-included", title: "context configuration", box: true }],
|
|
232
|
-
_setupArtifacts: [async ({ playwright, screenshot, resultScreenshots, _combinedContextOptions }, use, testInfo) => {
|
|
233
|
+
_setupArtifacts: [async ({ playwright, headless, screenshot, resultScreenshots, pauseOnFailure, _combinedContextOptions }, use, testInfo) => {
|
|
233
234
|
testInfo.setTimeout(testInfo.project.timeout);
|
|
234
|
-
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
|
|
235
|
+
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot, resultScreenshots, pauseOnFailure && !headless);
|
|
235
236
|
await artifactsRecorder.willStartTest(testInfo);
|
|
236
237
|
const tracingGroupSteps = [];
|
|
237
238
|
const csiListener = {
|
|
@@ -609,9 +610,11 @@ class SnapshotRecorder {
|
|
|
609
610
|
}
|
|
610
611
|
}
|
|
611
612
|
class ArtifactsRecorder {
|
|
612
|
-
constructor(playwright, artifactsDir, screenshot) {
|
|
613
|
+
constructor(playwright, artifactsDir, screenshot, resultScreenshots, pauseOnFailure) {
|
|
613
614
|
this._playwright = playwright;
|
|
614
615
|
this._artifactsDir = artifactsDir;
|
|
616
|
+
this._resultScreenshots = resultScreenshots;
|
|
617
|
+
this._pauseOnFailure = pauseOnFailure;
|
|
615
618
|
const screenshotOptions = typeof screenshot === "string" ? void 0 : screenshot;
|
|
616
619
|
this._startedCollectingArtifacts = Symbol("startedCollectingArtifacts");
|
|
617
620
|
this._screenshotRecorder = new SnapshotRecorder(this, normalizeScreenshotMode(screenshot), "screenshot", "image/png", ".png", async (page, path2) => {
|
|
@@ -661,6 +664,51 @@ class ArtifactsRecorder {
|
|
|
661
664
|
}
|
|
662
665
|
async didFinishTestFunction() {
|
|
663
666
|
await this._screenshotRecorder.maybeCapture();
|
|
667
|
+
if (this._resultScreenshots && this._testInfo._isFailure()) {
|
|
668
|
+
try {
|
|
669
|
+
const page = this._playwright._allPages()[0];
|
|
670
|
+
if (page) {
|
|
671
|
+
const pngBuffer = await page._wrapApiCall(async () => {
|
|
672
|
+
return await page.screenshot({ type: "png", timeout: 5e3 });
|
|
673
|
+
}, { internal: true });
|
|
674
|
+
const safeTestId = this._testInfo.testId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
675
|
+
const screenshotDir = import_path.default.join(process.cwd(), ".smoothdeploy-output", "screenshots", safeTestId);
|
|
676
|
+
import_fs.default.mkdirSync(screenshotDir, { recursive: true });
|
|
677
|
+
import_fs.default.writeFileSync(import_path.default.join(screenshotDir, "failure.png"), pngBuffer);
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (this._pauseOnFailure && this._testInfo._isFailure()) {
|
|
683
|
+
this._emitPauseOnFailure();
|
|
684
|
+
const page = this._playwright._allPages()[0];
|
|
685
|
+
if (page)
|
|
686
|
+
await page.pause();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
_emitPauseOnFailure() {
|
|
690
|
+
const findFailingPath = (steps) => {
|
|
691
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
692
|
+
const step = steps[i];
|
|
693
|
+
const childPath = findFailingPath(step.steps);
|
|
694
|
+
if (childPath.length > 0)
|
|
695
|
+
return [step.title, ...childPath];
|
|
696
|
+
if (step.error)
|
|
697
|
+
return [step.title];
|
|
698
|
+
}
|
|
699
|
+
return [];
|
|
700
|
+
};
|
|
701
|
+
const stepPath = findFailingPath(this._testInfo._steps);
|
|
702
|
+
const error = this._testInfo.errors[0];
|
|
703
|
+
process.stdout.write(`[SMOOTHDEPLOY_EVENT]${JSON.stringify({
|
|
704
|
+
event: "pauseOnFailure",
|
|
705
|
+
testId: this._testInfo.testId,
|
|
706
|
+
testTitle: this._testInfo.titlePath.slice(1).join(" > "),
|
|
707
|
+
stepPath,
|
|
708
|
+
error: error ? { message: error.message ?? String(error) } : null,
|
|
709
|
+
timestamp: Date.now()
|
|
710
|
+
})}
|
|
711
|
+
`);
|
|
664
712
|
}
|
|
665
713
|
async didFinishTest() {
|
|
666
714
|
await this.didFinishTestFunction();
|
|
@@ -760,7 +808,7 @@ async function captureResultScreenshot(channel, data, testInfo) {
|
|
|
760
808
|
const { full, cropped } = (0, import_screenshotCompositor.compositeHighlight)(pngBuffer, box, cssViewport);
|
|
761
809
|
const safeTestId = testInfo.testId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
762
810
|
const safeStepId = (data.stepId ?? "unknown").replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
763
|
-
const screenshotDir = import_path.default.join(process.cwd(), "
|
|
811
|
+
const screenshotDir = import_path.default.join(process.cwd(), ".smoothdeploy-output", "screenshots", safeTestId);
|
|
764
812
|
import_fs.default.mkdirSync(screenshotDir, { recursive: true });
|
|
765
813
|
const fullPng = new import_utilsBundle.PNG({ width: full.width, height: full.height });
|
|
766
814
|
fullPng.data = full.data;
|
|
@@ -37,8 +37,8 @@ class SmoothDeployReporter {
|
|
|
37
37
|
constructor() {
|
|
38
38
|
this._configDir = "";
|
|
39
39
|
this._manifest = null;
|
|
40
|
-
// Resolved manifest test for each active pw test
|
|
41
|
-
this.
|
|
40
|
+
// Resolved manifest test + paramIndex for each active pw test
|
|
41
|
+
this._resolvedTests = /* @__PURE__ */ new Map();
|
|
42
42
|
// Map pw TestStep → matched manifest step
|
|
43
43
|
this._stepToManifest = /* @__PURE__ */ new WeakMap();
|
|
44
44
|
// Child counter per parent (TestStep for nested, test.id string for root-level)
|
|
@@ -64,9 +64,9 @@ class SmoothDeployReporter {
|
|
|
64
64
|
this._loadManifest();
|
|
65
65
|
}
|
|
66
66
|
_loadManifest() {
|
|
67
|
-
const manifestPath = path.join(this._configDir, "smoothdeploy-manifest.json");
|
|
67
|
+
const manifestPath = path.join(this._configDir, ".smoothdeploy-manifest.json");
|
|
68
68
|
if (!fs.existsSync(manifestPath))
|
|
69
|
-
throw new Error(`SmoothDeploy reporter requires smoothdeploy-manifest.json in ${this._configDir}`);
|
|
69
|
+
throw new Error(`SmoothDeploy reporter requires .smoothdeploy-manifest.json in ${this._configDir}`);
|
|
70
70
|
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
71
71
|
this._manifest = JSON.parse(raw);
|
|
72
72
|
}
|
|
@@ -95,8 +95,9 @@ class SmoothDeployReporter {
|
|
|
95
95
|
return false;
|
|
96
96
|
return true;
|
|
97
97
|
}
|
|
98
|
-
// Resolve a pw TestCase to its manifest test entry
|
|
99
|
-
//
|
|
98
|
+
// Resolve a pw TestCase to its manifest test entry + param index.
|
|
99
|
+
// For parameterized tests: manifest title "Foo" with params=3 matches pw titles "Foo0", "Foo1", "Foo2".
|
|
100
|
+
// For non-parameterized tests: exact match, paramIndex=0.
|
|
100
101
|
_resolveManifestTest(test) {
|
|
101
102
|
if (!this._manifest)
|
|
102
103
|
return null;
|
|
@@ -117,30 +118,47 @@ class SmoothDeployReporter {
|
|
|
117
118
|
currentSuites = suite.suites;
|
|
118
119
|
currentTests = suite.tests;
|
|
119
120
|
}
|
|
120
|
-
|
|
121
|
+
const exactMatch = currentTests.find((t) => t.title === testTitle);
|
|
122
|
+
if (exactMatch)
|
|
123
|
+
return { manifestTest: exactMatch, paramIndex: 0 };
|
|
124
|
+
for (const mt of currentTests) {
|
|
125
|
+
if (!mt.params || mt.params < 1)
|
|
126
|
+
continue;
|
|
127
|
+
if (!testTitle.startsWith(mt.title))
|
|
128
|
+
continue;
|
|
129
|
+
const suffix = testTitle.slice(mt.title.length);
|
|
130
|
+
const idx = parseInt(suffix, 10);
|
|
131
|
+
if (String(idx) === suffix && idx >= 0 && idx < mt.params)
|
|
132
|
+
return { manifestTest: mt, paramIndex: idx };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
121
135
|
}
|
|
122
136
|
onTestBegin(test, result) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
const resolved = this._resolveManifestTest(test);
|
|
138
|
+
if (!resolved)
|
|
139
|
+
return;
|
|
140
|
+
this._resolvedTests.set(test.id, resolved);
|
|
141
|
+
const safePw = test.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
142
|
+
const safeManifest = resolved.manifestTest.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
143
|
+
this._testIdMap.set(safePw, safeManifest);
|
|
130
144
|
this._childCounters.set(test.id, 0);
|
|
131
145
|
this._testSteps.set(test.id, []);
|
|
132
146
|
this._emit({
|
|
133
147
|
event: "testBegin",
|
|
134
|
-
testId: manifestTest.id,
|
|
148
|
+
testId: resolved.manifestTest.id,
|
|
149
|
+
paramIndex: resolved.paramIndex,
|
|
135
150
|
testTitle: test.title,
|
|
136
151
|
timestamp: Date.now()
|
|
137
152
|
});
|
|
138
153
|
}
|
|
139
154
|
onTestEnd(test, result) {
|
|
140
|
-
const
|
|
155
|
+
const resolved = this._resolvedTests.get(test.id);
|
|
156
|
+
if (!resolved)
|
|
157
|
+
return;
|
|
141
158
|
this._emit({
|
|
142
159
|
event: "testEnd",
|
|
143
|
-
testId: manifestTest.id,
|
|
160
|
+
testId: resolved.manifestTest.id,
|
|
161
|
+
paramIndex: resolved.paramIndex,
|
|
144
162
|
testTitle: test.title,
|
|
145
163
|
status: result.status,
|
|
146
164
|
duration: result.duration,
|
|
@@ -151,13 +169,15 @@ class SmoothDeployReporter {
|
|
|
151
169
|
onStepBegin(test, result, step) {
|
|
152
170
|
if (!this._isUserStep(step))
|
|
153
171
|
return;
|
|
154
|
-
const
|
|
172
|
+
const resolved = this._resolvedTests.get(test.id);
|
|
173
|
+
if (!resolved)
|
|
174
|
+
return;
|
|
155
175
|
let parentStep = step.parent;
|
|
156
176
|
while (parentStep && !this._stepToOutput.has(parentStep))
|
|
157
177
|
parentStep = parentStep.parent;
|
|
158
178
|
const parentKey = parentStep ?? test.id;
|
|
159
179
|
const parentManifest = parentStep ? this._stepToManifest.get(parentStep) : void 0;
|
|
160
|
-
const parentSteps = parentManifest ? parentManifest.children : manifestTest
|
|
180
|
+
const parentSteps = parentManifest ? parentManifest.children : resolved.manifestTest.steps;
|
|
161
181
|
const idx = this._childCounters.get(parentKey) ?? 0;
|
|
162
182
|
this._childCounters.set(parentKey, idx + 1);
|
|
163
183
|
const manifestStep = parentSteps?.[idx] ?? null;
|
|
@@ -182,7 +202,8 @@ class SmoothDeployReporter {
|
|
|
182
202
|
this._testSteps.get(test.id)?.push(outputStep);
|
|
183
203
|
this._emit({
|
|
184
204
|
event: "stepBegin",
|
|
185
|
-
testId: manifestTest.id,
|
|
205
|
+
testId: resolved.manifestTest.id,
|
|
206
|
+
paramIndex: resolved.paramIndex,
|
|
186
207
|
id: manifestStep?.id,
|
|
187
208
|
action: manifestStep?.action,
|
|
188
209
|
title: step.title,
|
|
@@ -193,6 +214,9 @@ class SmoothDeployReporter {
|
|
|
193
214
|
onStepEnd(test, result, step) {
|
|
194
215
|
if (!this._isUserStep(step))
|
|
195
216
|
return;
|
|
217
|
+
const resolved = this._resolvedTests.get(test.id);
|
|
218
|
+
if (!resolved)
|
|
219
|
+
return;
|
|
196
220
|
const manifestStep = this._stepToManifest.get(step) ?? null;
|
|
197
221
|
const outputStep = this._stepToOutput.get(step);
|
|
198
222
|
const safeTestId = test.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
@@ -210,8 +234,8 @@ class SmoothDeployReporter {
|
|
|
210
234
|
const internalStepId = step._stepId ?? step.title;
|
|
211
235
|
const safeStepId = internalStepId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
212
236
|
const screenshots = hasScreenshot ? {
|
|
213
|
-
page:
|
|
214
|
-
element:
|
|
237
|
+
page: `.smoothdeploy-output/screenshots/${safeTestId}/${safeStepId}-page.png`,
|
|
238
|
+
element: `.smoothdeploy-output/screenshots/${safeTestId}/${safeStepId}-element.png`
|
|
215
239
|
} : null;
|
|
216
240
|
if (manifestStep) {
|
|
217
241
|
const safeManifestStepId = manifestStep.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
@@ -219,13 +243,15 @@ class SmoothDeployReporter {
|
|
|
219
243
|
}
|
|
220
244
|
if (outputStep) {
|
|
221
245
|
outputStep.duration = step.duration;
|
|
222
|
-
|
|
246
|
+
const hasDescendantError = (steps) => steps.some((c) => c.error !== null || hasDescendantError(c.children));
|
|
247
|
+
outputStep.error = step.error && !hasDescendantError(outputStep.children) ? { message: step.error.message } : null;
|
|
223
248
|
outputStep.screenshots = screenshots;
|
|
224
249
|
}
|
|
225
250
|
this._childCounters.delete(step);
|
|
226
251
|
this._emit({
|
|
227
252
|
event: "stepEnd",
|
|
228
|
-
testId:
|
|
253
|
+
testId: resolved.manifestTest.id,
|
|
254
|
+
paramIndex: resolved.paramIndex,
|
|
229
255
|
id: manifestStep?.id,
|
|
230
256
|
action: manifestStep?.action,
|
|
231
257
|
title: step.title,
|
|
@@ -235,7 +261,7 @@ class SmoothDeployReporter {
|
|
|
235
261
|
});
|
|
236
262
|
}
|
|
237
263
|
onEnd(result) {
|
|
238
|
-
const outputDir = path.join(process.cwd(), "
|
|
264
|
+
const outputDir = path.join(process.cwd(), ".smoothdeploy-output");
|
|
239
265
|
const screenshotsDir = path.join(outputDir, "screenshots");
|
|
240
266
|
if (this._manifest) {
|
|
241
267
|
for (const [safePwTestId] of this._testIdMap) {
|
|
@@ -254,8 +280,11 @@ class SmoothDeployReporter {
|
|
|
254
280
|
for (const [safePwTestId, safeManifestTestId] of this._testIdMap) {
|
|
255
281
|
const src = path.join(screenshotsDir, safePwTestId);
|
|
256
282
|
const dst = path.join(screenshotsDir, safeManifestTestId);
|
|
257
|
-
if (fs.existsSync(src))
|
|
283
|
+
if (fs.existsSync(src)) {
|
|
284
|
+
if (fs.existsSync(dst))
|
|
285
|
+
fs.rmSync(dst, { recursive: true });
|
|
258
286
|
fs.renameSync(src, dst);
|
|
287
|
+
}
|
|
259
288
|
}
|
|
260
289
|
}
|
|
261
290
|
const remapScreenshotPaths = (steps) => {
|
|
@@ -269,9 +298,17 @@ class SmoothDeployReporter {
|
|
|
269
298
|
};
|
|
270
299
|
const manifest = {};
|
|
271
300
|
for (const [testId, steps] of this._testSteps) {
|
|
272
|
-
const
|
|
301
|
+
const resolved = this._resolvedTests.get(testId);
|
|
302
|
+
if (!resolved)
|
|
303
|
+
continue;
|
|
273
304
|
remapScreenshotPaths(steps);
|
|
274
|
-
|
|
305
|
+
const safeManifestTestId = resolved.manifestTest.id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
306
|
+
const failurePath = path.join(screenshotsDir, safeManifestTestId, "failure.png");
|
|
307
|
+
const failureScreenshot = fs.existsSync(failurePath) ? `.smoothdeploy-output/screenshots/${safeManifestTestId}/failure.png` : null;
|
|
308
|
+
const key = resolved.manifestTest.id;
|
|
309
|
+
if (!manifest[key])
|
|
310
|
+
manifest[key] = [];
|
|
311
|
+
manifest[key].push({ paramIndex: resolved.paramIndex, steps, failureScreenshot });
|
|
275
312
|
}
|
|
276
313
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
277
314
|
fs.writeFileSync(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothdeploy/playwright",
|
|
3
|
-
"version": "1.58.
|
|
3
|
+
"version": "1.58.4",
|
|
4
4
|
"description": "A high-level API to automate web browsers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"license": "Apache-2.0",
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"playwright-core": "npm:@smoothdeploy/playwright-core@1.58.
|
|
67
|
+
"playwright-core": "npm:@smoothdeploy/playwright-core@1.58.4"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|
package/types/test.d.ts
CHANGED