@smoothdeploy/playwright 1.58.1 → 1.58.3

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 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,26 @@ 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
+ const page = this._playwright._allPages()[0];
684
+ if (page)
685
+ await page.pause();
686
+ }
664
687
  }
665
688
  async didFinishTest() {
666
689
  await this.didFinishTestFunction();
@@ -760,7 +783,7 @@ async function captureResultScreenshot(channel, data, testInfo) {
760
783
  const { full, cropped } = (0, import_screenshotCompositor.compositeHighlight)(pngBuffer, box, cssViewport);
761
784
  const safeTestId = testInfo.testId.replace(/[^a-zA-Z0-9_-]/g, "-");
762
785
  const safeStepId = (data.stepId ?? "unknown").replace(/[^a-zA-Z0-9_-]/g, "-");
763
- const screenshotDir = import_path.default.join(process.cwd(), "_internal_output", "screenshots", safeTestId);
786
+ const screenshotDir = import_path.default.join(process.cwd(), ".smoothdeploy-output", "screenshots", safeTestId);
764
787
  import_fs.default.mkdirSync(screenshotDir, { recursive: true });
765
788
  const fullPng = new import_utilsBundle.PNG({ width: full.width, height: full.height });
766
789
  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._manifestTests = /* @__PURE__ */ new Map();
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 using titlePath.
99
- // titlePath = ["", projectName, "file.spec.ts", ...describes..., "test title"]
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
- return currentTests.find((t) => t.title === testTitle) ?? null;
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 manifestTest = this._resolveManifestTest(test);
124
- if (manifestTest) {
125
- this._manifestTests.set(test.id, manifestTest);
126
- const safePw = test.id.replace(/[^a-zA-Z0-9_-]/g, "-");
127
- const safeManifest = manifestTest.id.replace(/[^a-zA-Z0-9_-]/g, "-");
128
- this._testIdMap.set(safePw, safeManifest);
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 manifestTest = this._manifestTests.get(test.id);
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 manifestTest = this._manifestTests.get(test.id);
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?.steps;
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: `_internal_output/screenshots/${safeTestId}/${safeStepId}-page.png`,
214
- element: `_internal_output/screenshots/${safeTestId}/${safeStepId}-element.png`
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
- outputStep.error = step.error ? { message: step.error.message } : null;
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: this._manifestTests.get(test.id).id,
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(), "_internal_output");
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 manifestTest = this._manifestTests.get(testId);
301
+ const resolved = this._resolvedTests.get(testId);
302
+ if (!resolved)
303
+ continue;
273
304
  remapScreenshotPaths(steps);
274
- manifest[manifestTest.id] = { steps };
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.1",
3
+ "version": "1.58.3",
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.1"
67
+ "playwright-core": "npm:@smoothdeploy/playwright-core@1.58.3"
68
68
  },
69
69
  "optionalDependencies": {
70
70
  "fsevents": "2.3.2"