@checkly/playwright-reporter 1.0.0 → 1.0.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@checkly/playwright-reporter` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## 1.0.2 (2026-01-19)
8
+
9
+ ### Fixed
10
+
11
+ - Test session creation is now properly awaited before uploading results, fixing intermittent upload failures in CI environments.
12
+
13
+ ## 1.0.1 (2026-01-19)
14
+
15
+ ### Fixed
16
+
17
+ - Sharded test execution now works correctly when using `playwright merge-reports` command. Due to a Playwright limitation, `config.projects` is empty after merging blob reports - the reporter now reconstructs project information from the test suite structure.
18
+ - Default projects (when no explicit projects are defined in playwright.config.ts) now preserve their empty name correctly instead of being renamed to "default".
19
+
20
+ ## 1.0.0 (2025-01-19)
21
+
22
+ First stable release of the rewritten Playwright reporter with extension-based architecture.
23
+
24
+ ### Added
25
+
26
+ - **Extension-based architecture** - Modular design allows for composable functionality
27
+ - **Broad Playwright support** - Compatible with Playwright versions 1.40 through 1.57+
28
+ - **Drop-in JSON replacement** - Fully compatible with Playwright's native JSON reporter output format
29
+ - **Trace extraction** - Automatically extracts console messages and network requests from Playwright trace files
30
+ - **Checkly integration** - Automatic upload to Checkly Test Sessions when credentials are configured
31
+ - **New `createChecklyReporter()` function** - Better intellisense support in `playwright.config.ts`
32
+ - **Unified `outputDir` option** - Single option for all output files (JSON report, ZIP assets)
33
+ - **Verbose logging** - Debug mode for troubleshooting report generation
34
+
35
+ ### Changed
36
+
37
+ - `outputFile` option is deprecated - use `outputDir` instead (JSON written to `{outputDir}/checkly-report.json`)
38
+ - `testResultsDir` option is deprecated - use `outputDir` instead
39
+ - `outputPath` option is deprecated - ZIP written to `{outputDir}/checkly-report.zip`
40
+
41
+ ### Migration from 0.x
42
+
43
+ ```diff
44
+ // playwright.config.ts
45
+ +import { createChecklyReporter } from '@checkly/playwright-reporter'
46
+
47
+ export default defineConfig({
48
+ reporter: [
49
+ - ['@checkly/playwright-reporter', { outputFile: 'results/report.json' }],
50
+ + createChecklyReporter({ outputDir: 'results' }),
51
+ ],
52
+ })
53
+ ```
54
+
55
+ The legacy 0.x reporter remains available via `npm install @checkly/playwright-reporter@legacy`.
package/dist/index.d.ts CHANGED
@@ -247,6 +247,7 @@ declare class BaseReporter implements Reporter {
247
247
  private _report;
248
248
  private extensions;
249
249
  private summaryLines;
250
+ private reconstructedProjects;
250
251
  constructor(options?: BaseReporterOptions);
251
252
  protected use(extension: Extension$1): this;
252
253
  private log;
@@ -264,6 +265,17 @@ declare class BaseReporter implements Reporter {
264
265
  printsToStdio(): boolean;
265
266
  getReport(): JSONReport;
266
267
  private buildReport;
268
+ /**
269
+ * Reconstructs projects from the suite structure when config.projects is empty.
270
+ * This handles the merge-reports scenario where Playwright doesn't populate
271
+ * config.projects but still provides project information via suite.project().
272
+ */
273
+ private reconstructProjectsIfNeeded;
274
+ /**
275
+ * Recursively collects project information from test cases in the suite.
276
+ * Fallback for when suite.project() doesn't provide project info.
277
+ */
278
+ private collectProjectsFromTests;
267
279
  private serializeConfig;
268
280
  private serializeProject;
269
281
  /**
@@ -289,6 +301,11 @@ declare class BaseReporter implements Reporter {
289
301
  */
290
302
  private extractTags;
291
303
  private serializeTest;
304
+ /**
305
+ * Get the project name for a test case, handling merge-reports scenarios
306
+ * where project() may not be available in the expected way.
307
+ */
308
+ private getProjectName;
292
309
  private serializeTestResult;
293
310
  private serializeStep;
294
311
  private serializeError;
package/dist/index.js CHANGED
@@ -1598,6 +1598,7 @@ function checklyUpload(options = {}) {
1598
1598
  }
1599
1599
  let api;
1600
1600
  let testSession;
1601
+ let sessionCreationPromise;
1601
1602
  let startTime;
1602
1603
  let resolvedOutputDir;
1603
1604
  let assetCollector;
@@ -1616,6 +1617,26 @@ function checklyUpload(options = {}) {
1616
1617
  if (typeof sessionName === "string") return sessionName;
1617
1618
  return `Playwright Test Session: ${ctx.directoryName}`;
1618
1619
  }
1620
+ async function createSession(config, suite, log) {
1621
+ if (!api) return;
1622
+ try {
1623
+ const directoryName = getDirectoryName();
1624
+ const sessionName = resolveSessionName({ directoryName, config, suite });
1625
+ const repoInfo = getGitHubRepoInfo();
1626
+ log("\u{1F517} Creating test session", { name: sessionName, environment });
1627
+ testSession = await api.testSessions.create({
1628
+ name: sessionName,
1629
+ environment: process.env.NODE_ENV || "test",
1630
+ repoInfo,
1631
+ startedAt: startTime.getTime(),
1632
+ testResults: [{ name: directoryName }],
1633
+ provider: "PW_REPORTER"
1634
+ });
1635
+ log("\u2705 Session created", { id: testSession.testSessionId });
1636
+ } catch (err2) {
1637
+ console.error("[Checkly] Failed to create test session:", err2);
1638
+ }
1639
+ }
1619
1640
  function checkTraceAttachment(test, result) {
1620
1641
  const key = `${test.id}:${result.retry}`;
1621
1642
  const traceAttachment = result.attachments?.find((a) => a.name === "trace" || a.contentType === "application/zip");
@@ -1740,7 +1761,7 @@ function checklyUpload(options = {}) {
1740
1761
  }
1741
1762
  return {
1742
1763
  name: "checkly-upload",
1743
- onBegin: async ({ config, suite, log }) => {
1764
+ onBegin: ({ config, suite, log }) => {
1744
1765
  startTime = /* @__PURE__ */ new Date();
1745
1766
  resolvedOutputDir = options.outputDir ?? options.testResultsDir ?? config.projects[0]?.outputDir ?? "test-results";
1746
1767
  const zipPath = options.outputPath ?? path3.join(resolvedOutputDir, "checkly-report.zip");
@@ -1752,23 +1773,7 @@ function checklyUpload(options = {}) {
1752
1773
  }
1753
1774
  return;
1754
1775
  }
1755
- try {
1756
- const directoryName = getDirectoryName();
1757
- const sessionName = resolveSessionName({ directoryName, config, suite });
1758
- const repoInfo = getGitHubRepoInfo();
1759
- log("\u{1F517} Creating test session", { name: sessionName, environment });
1760
- testSession = await api.testSessions.create({
1761
- name: sessionName,
1762
- environment: process.env.NODE_ENV || "test",
1763
- repoInfo,
1764
- startedAt: startTime.getTime(),
1765
- testResults: [{ name: directoryName }],
1766
- provider: "PW_REPORTER"
1767
- });
1768
- log("\u2705 Session created", { id: testSession.testSessionId });
1769
- } catch (err2) {
1770
- console.error("[Checkly] Failed to create test session:", err2);
1771
- }
1776
+ sessionCreationPromise = createSession(config, suite, log);
1772
1777
  },
1773
1778
  onTestEnd: ({ test, result }) => {
1774
1779
  checkTraceAttachment(test, result);
@@ -1786,6 +1791,9 @@ function checklyUpload(options = {}) {
1786
1791
  },
1787
1792
  onEnd: async ({ report, log, addSummaryLine }) => {
1788
1793
  try {
1794
+ if (sessionCreationPromise) {
1795
+ await sessionCreationPromise;
1796
+ }
1789
1797
  await extractDataFromTraces(log);
1790
1798
  injectDataIntoReport(report);
1791
1799
  fs4.mkdirSync(resolvedOutputDir, { recursive: true });
@@ -1855,6 +1863,8 @@ var BaseReporter = class {
1855
1863
  _report = null;
1856
1864
  extensions = [];
1857
1865
  summaryLines = [];
1866
+ // Reconstructed projects for merge-reports scenarios
1867
+ reconstructedProjects = /* @__PURE__ */ new Map();
1858
1868
  constructor(options = {}) {
1859
1869
  this.options = options;
1860
1870
  this.verbose = options.verbose ?? process.env.CHECKLY_REPORTER_VERBOSE === "true";
@@ -1889,8 +1899,10 @@ var BaseReporter = class {
1889
1899
  this.flakyCount = 0;
1890
1900
  this.skippedCount = 0;
1891
1901
  this._report = null;
1902
+ this.reconstructedProjects.clear();
1903
+ this.reconstructProjectsIfNeeded();
1892
1904
  const testCount = this.countTests(suite);
1893
- const projectNames = config.projects.map((p) => p.name).join(", ");
1905
+ const projectNames = config.projects.length > 0 ? config.projects.map((p) => p.name).join(", ") : Array.from(this.reconstructedProjects.keys()).join(", ") || "default";
1894
1906
  this.log(`\u{1F3AC} Starting test run`, { tests: testCount, projects: projectNames, workers: config.workers });
1895
1907
  for (const ext of this.extensions) {
1896
1908
  ext.onBegin?.({ config, suite, log: this.createExtensionLogger(ext.name) });
@@ -2022,8 +2034,86 @@ var BaseReporter = class {
2022
2034
  }
2023
2035
  };
2024
2036
  }
2037
+ /**
2038
+ * Reconstructs projects from the suite structure when config.projects is empty.
2039
+ * This handles the merge-reports scenario where Playwright doesn't populate
2040
+ * config.projects but still provides project information via suite.project().
2041
+ */
2042
+ reconstructProjectsIfNeeded() {
2043
+ if (this.config.projects.length > 0) {
2044
+ return;
2045
+ }
2046
+ this.log("\u{1F504} Detected merge-reports scenario (empty config.projects), reconstructing projects");
2047
+ for (const projectSuite of this.suite.suites) {
2048
+ const project = projectSuite.project();
2049
+ if (project) {
2050
+ const name = project.name || "default";
2051
+ if (!this.reconstructedProjects.has(name)) {
2052
+ this.reconstructedProjects.set(name, {
2053
+ id: name,
2054
+ name,
2055
+ testDir: project.testDir,
2056
+ outputDir: project.outputDir,
2057
+ timeout: project.timeout,
2058
+ retries: project.retries,
2059
+ repeatEach: project.repeatEach,
2060
+ metadata: project.metadata ?? {},
2061
+ testMatch: Array.isArray(project.testMatch) ? project.testMatch.map(String) : [String(project.testMatch)],
2062
+ testIgnore: Array.isArray(project.testIgnore) ? project.testIgnore.map(String) : [String(project.testIgnore)]
2063
+ });
2064
+ }
2065
+ }
2066
+ }
2067
+ if (this.reconstructedProjects.size === 0) {
2068
+ this.collectProjectsFromTests(this.suite);
2069
+ }
2070
+ this.log("\u{1F504} Reconstructed projects", {
2071
+ count: this.reconstructedProjects.size,
2072
+ names: Array.from(this.reconstructedProjects.keys())
2073
+ });
2074
+ }
2075
+ /**
2076
+ * Recursively collects project information from test cases in the suite.
2077
+ * Fallback for when suite.project() doesn't provide project info.
2078
+ */
2079
+ collectProjectsFromTests(suite) {
2080
+ for (const test of suite.allTests()) {
2081
+ const project = test.parent.project();
2082
+ const projectName = project?.name || "default";
2083
+ if (!this.reconstructedProjects.has(projectName)) {
2084
+ if (project) {
2085
+ this.reconstructedProjects.set(projectName, {
2086
+ id: projectName,
2087
+ name: projectName,
2088
+ testDir: project.testDir,
2089
+ outputDir: project.outputDir,
2090
+ timeout: project.timeout,
2091
+ retries: project.retries,
2092
+ repeatEach: project.repeatEach,
2093
+ metadata: project.metadata ?? {},
2094
+ testMatch: Array.isArray(project.testMatch) ? project.testMatch.map(String) : [String(project.testMatch)],
2095
+ testIgnore: Array.isArray(project.testIgnore) ? project.testIgnore.map(String) : [String(project.testIgnore)]
2096
+ });
2097
+ } else {
2098
+ this.reconstructedProjects.set(projectName, {
2099
+ id: projectName,
2100
+ name: projectName,
2101
+ testDir: "",
2102
+ outputDir: "",
2103
+ timeout: 0,
2104
+ retries: 0,
2105
+ repeatEach: 1,
2106
+ metadata: {},
2107
+ testMatch: [],
2108
+ testIgnore: []
2109
+ });
2110
+ }
2111
+ }
2112
+ }
2113
+ }
2025
2114
  serializeConfig() {
2026
2115
  const c = this.config;
2116
+ const projects = c.projects.length > 0 ? c.projects.map((p) => this.serializeProject(p)) : Array.from(this.reconstructedProjects.values());
2027
2117
  return {
2028
2118
  rootDir: c.rootDir,
2029
2119
  configFile: c.configFile ?? void 0,
@@ -2034,7 +2124,7 @@ var BaseReporter = class {
2034
2124
  globalTimeout: c.globalTimeout,
2035
2125
  maxFailures: c.maxFailures,
2036
2126
  metadata: c.metadata ?? {},
2037
- projects: c.projects.map((p) => this.serializeProject(p)),
2127
+ projects,
2038
2128
  shard: c.shard ?? null,
2039
2129
  tags: c.tags ?? [],
2040
2130
  updateSourceMethod: c.updateSourceMethod,
@@ -2162,9 +2252,10 @@ var BaseReporter = class {
2162
2252
  }
2163
2253
  serializeTest(data) {
2164
2254
  const { testCase, results } = data;
2255
+ const projectName = this.getProjectName(testCase);
2165
2256
  return {
2166
- projectId: testCase.parent.project()?.name ?? "default",
2167
- projectName: testCase.parent.project()?.name ?? "default",
2257
+ projectId: projectName,
2258
+ projectName,
2168
2259
  timeout: testCase.timeout,
2169
2260
  expectedStatus: testCase.expectedStatus,
2170
2261
  annotations: this.serializeAnnotations(testCase.annotations),
@@ -2172,6 +2263,25 @@ var BaseReporter = class {
2172
2263
  status: this.mapOutcome(testCase.outcome())
2173
2264
  };
2174
2265
  }
2266
+ /**
2267
+ * Get the project name for a test case, handling merge-reports scenarios
2268
+ * where project() may not be available in the expected way.
2269
+ */
2270
+ getProjectName(testCase) {
2271
+ const project = testCase.parent.project();
2272
+ if (project && typeof project.name === "string") {
2273
+ return project.name;
2274
+ }
2275
+ let current = testCase.parent;
2276
+ while (current) {
2277
+ const suiteProject = current.project();
2278
+ if (suiteProject && typeof suiteProject.name === "string") {
2279
+ return suiteProject.name;
2280
+ }
2281
+ current = current.parent;
2282
+ }
2283
+ return "default";
2284
+ }
2175
2285
  serializeTestResult(r) {
2176
2286
  return {
2177
2287
  workerIndex: r.workerIndex,
@@ -2255,9 +2365,9 @@ var BaseReporter = class {
2255
2365
  printSummary() {
2256
2366
  const pkgVersion = getPackageVersion();
2257
2367
  const playwrightVersion = this.config.version;
2258
- const projects = this.config.projects;
2259
- const projectNames = projects.map((p) => p.name).join(", ");
2260
- const rule = pluralRules.select(projects.length);
2368
+ const projectNames = this.config.projects.length > 0 ? this.config.projects.map((p) => p.name).join(", ") : Array.from(this.reconstructedProjects.keys()).join(", ") || "default";
2369
+ const projectCount = this.config.projects.length > 0 ? this.config.projects.length : this.reconstructedProjects.size || 1;
2370
+ const rule = pluralRules.select(projectCount);
2261
2371
  console.log("\n======================================================\n");
2262
2372
  console.log(`\u{1F99D} Checkly reporter: ${pkgVersion}`);
2263
2373
  console.log(`\u{1F3AD} Playwright: ${playwrightVersion}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkly/playwright-reporter",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Standalone Playwright reporter for Checkly - compatible with Playwright 1.40-1.57+",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "dist",
10
10
  "README.md",
11
+ "CHANGELOG.md",
11
12
  "LICENSE"
12
13
  ],
13
14
  "keywords": [
@@ -35,7 +36,7 @@
35
36
  "tsup": "8.5.1",
36
37
  "tsx": "4.19.0",
37
38
  "typescript": "5.9.3",
38
- "@checkly/reporter-utils": "1.0.0"
39
+ "@checkly/reporter-utils": "1.0.2"
39
40
  },
40
41
  "scripts": {
41
42
  "build": "tsup",