@midscene/core 1.17.4 → 1.17.5

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.
Files changed (86) hide show
  1. package/dist/es/agent/agent-report-manager.mjs +28 -5
  2. package/dist/es/agent/agent-report-manager.mjs.map +1 -1
  3. package/dist/es/agent/agent.mjs +2 -2
  4. package/dist/es/agent/agent.mjs.map +1 -1
  5. package/dist/es/agent/utils.mjs +1 -1
  6. package/dist/es/dump/html-utils.mjs +51 -68
  7. package/dist/es/dump/html-utils.mjs.map +1 -1
  8. package/dist/es/dump/report-artifact-publication.mjs +52 -0
  9. package/dist/es/dump/report-artifact-publication.mjs.map +1 -0
  10. package/dist/es/dump/report-artifact-store.mjs +2 -2
  11. package/dist/es/dump/report-artifact-store.mjs.map +1 -1
  12. package/dist/es/dump/report-artifact-writer.mjs +129 -0
  13. package/dist/es/dump/report-artifact-writer.mjs.map +1 -0
  14. package/dist/es/dump/report-session.mjs +229 -0
  15. package/dist/es/dump/report-session.mjs.map +1 -0
  16. package/dist/es/dump/test-report-images.mjs +68 -42
  17. package/dist/es/dump/test-report-images.mjs.map +1 -1
  18. package/dist/es/index.mjs +2 -2
  19. package/dist/es/index.mjs.map +1 -1
  20. package/dist/es/report-cli.mjs +38 -11
  21. package/dist/es/report-cli.mjs.map +1 -1
  22. package/dist/es/report-generator.mjs +2 -1
  23. package/dist/es/report-generator.mjs.map +1 -1
  24. package/dist/es/report-html-template.mjs +1 -1
  25. package/dist/es/report.mjs +152 -159
  26. package/dist/es/report.mjs.map +1 -1
  27. package/dist/es/test-run-report.mjs.map +1 -1
  28. package/dist/es/utils.mjs +1 -1
  29. package/dist/es/yaml/player.mjs +1 -1
  30. package/dist/es/yaml/player.mjs.map +1 -1
  31. package/dist/lib/agent/agent-report-manager.js +27 -4
  32. package/dist/lib/agent/agent-report-manager.js.map +1 -1
  33. package/dist/lib/agent/agent.js +2 -2
  34. package/dist/lib/agent/agent.js.map +1 -1
  35. package/dist/lib/agent/utils.js +1 -1
  36. package/dist/lib/dump/html-utils.js +59 -70
  37. package/dist/lib/dump/html-utils.js.map +1 -1
  38. package/dist/lib/dump/report-artifact-publication.js +89 -0
  39. package/dist/lib/dump/report-artifact-publication.js.map +1 -0
  40. package/dist/lib/dump/report-artifact-store.js +2 -2
  41. package/dist/lib/dump/report-artifact-store.js.map +1 -1
  42. package/dist/lib/dump/report-artifact-writer.js +163 -0
  43. package/dist/lib/dump/report-artifact-writer.js.map +1 -0
  44. package/dist/lib/dump/report-session.js +266 -0
  45. package/dist/lib/dump/report-session.js.map +1 -0
  46. package/dist/lib/dump/test-report-images.js +67 -41
  47. package/dist/lib/dump/test-report-images.js.map +1 -1
  48. package/dist/lib/index.js +3 -0
  49. package/dist/lib/index.js.map +1 -1
  50. package/dist/lib/report-cli.js +37 -10
  51. package/dist/lib/report-cli.js.map +1 -1
  52. package/dist/lib/report-generator.js +2 -1
  53. package/dist/lib/report-generator.js.map +1 -1
  54. package/dist/lib/report-html-template.js +1 -1
  55. package/dist/lib/report.js +152 -153
  56. package/dist/lib/report.js.map +1 -1
  57. package/dist/lib/test-run-report.js.map +1 -1
  58. package/dist/lib/utils.js +1 -1
  59. package/dist/lib/yaml/player.js +1 -1
  60. package/dist/lib/yaml/player.js.map +1 -1
  61. package/dist/types/agent/agent-report-manager.d.ts +4 -1
  62. package/dist/types/agent/agent-report-manager.d.ts.map +1 -1
  63. package/dist/types/agent/agent.d.ts +1 -1
  64. package/dist/types/agent/agent.d.ts.map +1 -1
  65. package/dist/types/dump/html-utils.d.ts +7 -1
  66. package/dist/types/dump/html-utils.d.ts.map +1 -1
  67. package/dist/types/dump/report-artifact-publication.d.ts +5 -0
  68. package/dist/types/dump/report-artifact-publication.d.ts.map +1 -0
  69. package/dist/types/dump/report-artifact-store.d.ts +1 -1
  70. package/dist/types/dump/report-artifact-store.d.ts.map +1 -1
  71. package/dist/types/dump/report-artifact-writer.d.ts +21 -0
  72. package/dist/types/dump/report-artifact-writer.d.ts.map +1 -0
  73. package/dist/types/dump/report-session.d.ts +48 -0
  74. package/dist/types/dump/report-session.d.ts.map +1 -0
  75. package/dist/types/dump/test-report-images.d.ts +5 -2
  76. package/dist/types/dump/test-report-images.d.ts.map +1 -1
  77. package/dist/types/index.d.ts +1 -1
  78. package/dist/types/index.d.ts.map +1 -1
  79. package/dist/types/report-cli.d.ts +2 -1
  80. package/dist/types/report-cli.d.ts.map +1 -1
  81. package/dist/types/report-generator.d.ts.map +1 -1
  82. package/dist/types/report.d.ts +11 -0
  83. package/dist/types/report.d.ts.map +1 -1
  84. package/dist/types/test-run-report.d.ts +2 -2
  85. package/dist/types/test-run-report.d.ts.map +1 -1
  86. package/package.json +2 -2
@@ -1,21 +1,23 @@
1
- import { appendFileSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { appendFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { basename, dirname, join, resolve } from "node:path";
1
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve, sep } from "node:path";
4
4
  import { getMidsceneRunSubDir } from "@midscene/shared/common";
5
5
  import { antiEscapeScriptTag, logMsg, uuid } from "@midscene/shared/utils";
6
6
  import { getReportFileName } from "./agent/report-file-name.mjs";
7
- import { DATA_SCREENSHOT_MODE_ATTR, extractLastDumpScriptSync, generateAgentReportComment, generateTestRunReportScriptTag, getBaseUrlFixScript, streamDumpScriptsSync, streamImageScriptsToFile } from "./dump/html-utils.mjs";
7
+ import { DATA_SCREENSHOT_MODE_ATTR, extractLastDumpScriptSync, generateAgentReportComment, streamDumpScriptsSync } from "./dump/html-utils.mjs";
8
8
  import { imageRefFileExtension, normalizeStoredImageRef } from "./dump/image-reference.mjs";
9
9
  import { readReportActionDump, readReportArtifactManifest, readReportArtifactManifestAsync, readReportDumpScripts, readReportRunnerDump, streamReportDumpScripts } from "./dump/report-artifact-reader.mjs";
10
10
  import { parseBase64ImageDataUrl, resolveImageSource } from "./dump/screenshot-store.mjs";
11
11
  import { ExecutionRecorder } from "./dump/execution-recorder.mjs";
12
12
  import { ReportArtifactStore } from "./dump/report-artifact-store.mjs";
13
- import { TestReportImages } from "./dump/test-report-images.mjs";
13
+ import { ReportSession } from "./dump/report-session.mjs";
14
+ import { publishReportArtifact, publishReportArtifactSync } from "./dump/report-artifact-publication.mjs";
15
+ import { ReportArtifactWriter } from "./dump/report-artifact-writer.mjs";
14
16
  import { collectReportSummary } from "./report-stats.mjs";
15
17
  import { mergeTestRunReportDumps } from "./test-runner/reporting/merge-report.mjs";
16
18
  import { collectTestRunReportMetrics } from "./test-runner/reporting/metrics.mjs";
17
19
  import { ReportActionDump } from "./types.mjs";
18
- import { getReportTpl, getVersion, reportHTMLContent } from "./utils.mjs";
20
+ import { getVersion } from "./utils.mjs";
19
21
  import { calculateTestRunHealth, classifyTestRunCase } from "./test-run-health.mjs";
20
22
  function _define_property(obj, key, value) {
21
23
  if (key in obj) Object.defineProperty(obj, key, {
@@ -27,6 +29,64 @@ function _define_property(obj, key, value) {
27
29
  else obj[key] = value;
28
30
  return obj;
29
31
  }
32
+ async function exportReportArtifact(options) {
33
+ validateTestRunReportFileName(options.reportFileName);
34
+ const input = resolve(options.reportPath);
35
+ const output = resolve(options.outputDir);
36
+ const external = 'html-and-external-assets' === options.outputFormat;
37
+ const artifact = join(output, options.reportFileName);
38
+ const destination = external ? join(artifact, 'index.html') : `${artifact}.html`;
39
+ if (input === destination || external && input.startsWith(`${artifact}${sep}`)) throw new Error('Export destination must differ from the input report.');
40
+ if (!options.overwrite && existsSync(external ? artifact : destination)) throw new Error(`Report artifact already exists: ${artifact}`);
41
+ const manifest = readReportArtifactManifest(input);
42
+ const runner = readReportRunnerDump(input);
43
+ const groups = new Map();
44
+ for (const source of readReportDumpScripts(input)){
45
+ const attributes = Object.fromEntries([
46
+ ...source.openTag.matchAll(/([^\s=]+)="([^"]*)"/g)
47
+ ].filter(([, key])=>'type' !== key).map(([, key, value])=>[
48
+ key,
49
+ decodeURIComponent(value)
50
+ ]));
51
+ const id = attributes['data-group-id'];
52
+ if (!id) throw new Error('Report execution group is missing its ID.');
53
+ const dump = ReportActionDump.fromSerializedString(manifest ? source.content : antiEscapeScriptTag(source.content));
54
+ const previous = groups.get(id);
55
+ dump.executions = dedupeExecutionsKeepLatest([
56
+ ...previous?.dump.executions ?? [],
57
+ ...dump.executions
58
+ ]);
59
+ groups.set(id, {
60
+ dump,
61
+ attributes
62
+ });
63
+ }
64
+ if (!groups.size && !runner) throw new Error('No report execution data was found in the input.');
65
+ const hint = [
66
+ ...groups.values()
67
+ ][0]?.attributes['data-report-kind'];
68
+ const kind = manifest?.kind ?? ('agent' === hint || 'test' === hint ? hint : runner ? 'test' : 'agent');
69
+ await mkdir(output, {
70
+ recursive: true
71
+ });
72
+ const temporary = await mkdtemp(join(output, '.report-export-'));
73
+ const entry = join(temporary, 'index.html');
74
+ try {
75
+ const writer = new ReportArtifactWriter(entry, external, kind, manifest?.reportId ?? runner?.runId);
76
+ await writer.initialize();
77
+ for (const [id, group] of groups)await writer.addGroup(id, group.dump.serialize(), input, group.attributes);
78
+ await writer.finish(runner, mergedAgentReportComment([
79
+ ...groups.values()
80
+ ].map((group)=>group.dump)));
81
+ await publishReportArtifact(external ? temporary : entry, external ? artifact : destination, external, options.overwrite);
82
+ return destination;
83
+ } finally{
84
+ await rm(temporary, {
85
+ recursive: true,
86
+ force: true
87
+ });
88
+ }
89
+ }
30
90
  const screenshotModeAttrRegExp = new RegExp(`${DATA_SCREENSHOT_MODE_ATTR}="(inline|directory)"`);
31
91
  function readDeclaredScreenshotMode(reportFilePath) {
32
92
  let mode;
@@ -130,8 +190,8 @@ class ReportMergingTool {
130
190
  clear() {
131
191
  this.reportInfos = [];
132
192
  }
133
- mergeDumpScripts(contents) {
134
- const unescaped = contents.map((c)=>antiEscapeScriptTag(c)).filter((c)=>c.length > 0);
193
+ mergeDumpScripts(contents, external) {
194
+ const unescaped = contents.map((c)=>external ? c : antiEscapeScriptTag(c)).filter((c)=>c.length > 0);
135
195
  if (0 === unescaped.length) return '';
136
196
  if (1 === unescaped.length) return unescaped[0];
137
197
  const base = ReportActionDump.fromSerializedString(unescaped[0]);
@@ -145,99 +205,67 @@ class ReportMergingTool {
145
205
  base.executions = dedupeExecutionsKeepLatest(allExecutions);
146
206
  return base.serialize();
147
207
  }
148
- mergeReports(reportFileName = 'AUTO', opts) {
149
- const { rmOriginalReports = false, overwrite = false, outputDir } = opts ?? {};
150
- if (0 === this.reportInfos.length) {
208
+ mergeReports(reportFileName = 'AUTO', opts = {}) {
209
+ const { rmOriginalReports = false, overwrite = false, outputDir, outputFormat } = opts;
210
+ if (!this.reportInfos.length) {
151
211
  logMsg('No reports to merge');
152
212
  return null;
153
213
  }
154
214
  const runnerDumps = this.reportInfos.map((info)=>info.reportFilePath ? readReportRunnerDump(info.reportFilePath) : void 0);
155
215
  const hasRunnerDump = runnerDumps.some(Boolean);
216
+ const external = 'html-and-external-assets' === outputFormat;
217
+ const name = 'AUTO' === reportFileName ? getReportFileName('merged-report') : reportFileName;
218
+ validateTestRunReportFileName(name);
156
219
  const targetDir = outputDir ? resolve(outputDir) : getMidsceneRunSubDir('report');
157
- if (outputDir) mkdirSync(targetDir, {
158
- recursive: true
159
- });
160
- const isDirModeByIndex = this.reportInfos.map((info)=>Boolean(info.reportFilePath && isDirectoryModeReport(info.reportFilePath)));
161
- const hasDirectoryModeReport = isDirModeByIndex.some(Boolean);
162
- const resolvedName = 'AUTO' === reportFileName ? getReportFileName('merged-report') : reportFileName;
163
- const outputFilePath = hasDirectoryModeReport ? resolve(targetDir, resolvedName, 'index.html') : resolve(targetDir, `${resolvedName}.html`);
164
- if ('AUTO' !== reportFileName && existsSync(outputFilePath)) {
165
- if (!overwrite) throw new Error(`Report file already exists: ${outputFilePath}\nSet overwrite to true to overwrite this file.`);
166
- if (hasDirectoryModeReport) rmSync(dirname(outputFilePath), {
167
- recursive: true,
168
- force: true
169
- });
170
- else unlinkSync(outputFilePath);
171
- }
172
- if (hasDirectoryModeReport) mkdirSync(dirname(outputFilePath), {
220
+ mkdirSync(targetDir, {
173
221
  recursive: true
174
222
  });
175
- logMsg(`Start merging ${this.reportInfos.length} reports...\nCreating template file...`);
223
+ const artifact = join(targetDir, name);
224
+ const outputFilePath = external ? join(artifact, 'index.html') : `${artifact}.html`;
225
+ if (!overwrite && existsSync(external ? artifact : outputFilePath)) throw new Error(`Report file already exists: ${outputFilePath}\nSet overwrite to true to overwrite this file.`);
226
+ if (rmOriginalReports && this.reportInfos.some((info)=>info.reportFilePath && isDirectoryBasedReport(info.reportFilePath) && outputFilePath.startsWith(`${dirname(resolve(info.reportFilePath))}${sep}`))) throw new Error('Merged output must be outside source report directories when removing originals.');
227
+ const temporary = mkdtempSync(join(targetDir, '.merged-report-'));
228
+ const entry = join(temporary, 'index.html');
176
229
  try {
177
- const htmlEndTag = '</html>';
178
- const tpl = getReportTpl();
179
- const htmlEndIdx = tpl.lastIndexOf(htmlEndTag);
180
- const tplWithoutClose = -1 !== htmlEndIdx ? tpl.slice(0, htmlEndIdx) : tpl;
181
- appendFileSync(outputFilePath, tplWithoutClose);
182
- if (hasDirectoryModeReport) appendFileSync(outputFilePath, getBaseUrlFixScript());
230
+ const writer = new ReportArtifactWriter(entry, external, hasRunnerDump ? 'test' : 'agent');
231
+ writer.initializeSync();
183
232
  const agentReports = [];
184
233
  const runnerSources = [];
185
- const writtenInlineImageIds = new Set();
186
- for(let i = 0; i < this.reportInfos.length; i++){
187
- const reportInfo = this.reportInfos[i];
188
- logMsg(`Processing report ${i + 1}/${this.reportInfos.length}`);
189
- const { reportAttributes } = reportInfo;
190
- let dumpString = this.createEmptyDumpString(reportAttributes.testTitle, reportAttributes.testDescription);
191
- let mergedGroupId = `merged-group-${i}`;
192
- if (reportInfo.reportFilePath) {
193
- if (isDirModeByIndex[i]) {
194
- const reportDir = dirname(reportInfo.reportFilePath);
195
- const screenshotsDir = join(reportDir, 'screenshots');
196
- if (existsSync(screenshotsDir)) {
197
- const mergedScreenshotsDir = join(dirname(outputFilePath), 'screenshots');
198
- mkdirSync(mergedScreenshotsDir, {
199
- recursive: true
200
- });
201
- for (const file of readdirSync(screenshotsDir)){
202
- const src = join(screenshotsDir, file);
203
- const dest = join(mergedScreenshotsDir, file);
204
- copyFileSync(src, dest);
205
- }
206
- }
207
- } else streamImageScriptsToFile(reportInfo.reportFilePath, outputFilePath, writtenInlineImageIds);
208
- const allDumps = readReportDumpScripts(reportInfo.reportFilePath).filter((d)=>d.openTag.includes('data-group-id'));
209
- const groupIdMatch = allDumps[0]?.openTag.match(/data-group-id="([^"]+)"/);
210
- if (groupIdMatch && !hasRunnerDump) mergedGroupId = decodeURIComponent(groupIdMatch[1]);
211
- const extractedDumpString = allDumps.length > 0 ? this.mergeDumpScripts(allDumps.map((d)=>d.content)) : extractLastDumpScriptSync(reportInfo.reportFilePath);
212
- if (extractedDumpString) dumpString = extractedDumpString;
234
+ const groupIds = new Set();
235
+ for (const [index, info] of this.reportInfos.entries()){
236
+ const attributes = info.reportAttributes;
237
+ let serialized = this.createEmptyDumpString(attributes.testTitle, attributes.testDescription);
238
+ let groupId = `merged-group-${index}`;
239
+ if (info.reportFilePath) {
240
+ const dumps = readReportDumpScripts(info.reportFilePath);
241
+ const originalId = dumps[0]?.openTag.match(/data-group-id="([^"]+)"/)?.[1];
242
+ if (originalId && !hasRunnerDump && !groupIds.has(decodeURIComponent(originalId))) groupId = decodeURIComponent(originalId);
243
+ serialized = dumps.length ? this.mergeDumpScripts(dumps.map((dump)=>dump.content), Boolean(readReportArtifactManifest(info.reportFilePath))) : extractLastDumpScriptSync(info.reportFilePath) ?? serialized;
213
244
  }
214
- const agentReport = tryParseAgentReportDump(dumpString);
245
+ if (groupIds.has(groupId)) groupId = `merged-group-${uuid()}`;
246
+ groupIds.add(groupId);
247
+ const agentReport = tryParseAgentReportDump(serialized);
215
248
  if (agentReport) agentReports.push(agentReport);
216
249
  if (hasRunnerDump) runnerSources.push({
217
- reportId: mergedGroupId,
218
- sourcePath: reportInfo.reportFilePath ?? '<skipped-agent-report>',
219
- attributes: reportAttributes,
220
- dump: runnerDumps[i],
221
- timing: agentReport ? agentReportTiming(agentReport, reportAttributes.testDuration) : void 0
250
+ reportId: groupId,
251
+ sourcePath: info.reportFilePath ?? '<skipped-agent-report>',
252
+ attributes,
253
+ dump: runnerDumps[index],
254
+ timing: agentReport ? agentReportTiming(agentReport, attributes.testDuration) : void 0
255
+ });
256
+ writer.addGroupSync(groupId, serialized, info.reportFilePath ?? entry, {
257
+ ...hasRunnerDump ? {
258
+ 'data-report-id': groupId
259
+ } : {},
260
+ playwright_test_duration: attributes.testDuration,
261
+ playwright_test_status: attributes.testStatus,
262
+ playwright_test_title: attributes.testTitle,
263
+ playwright_test_id: attributes.testId,
264
+ playwright_test_description: attributes.testDescription,
265
+ is_merged: true
222
266
  });
223
- const reportHtmlStr = `${reportHTMLContent({
224
- dumpString,
225
- attributes: {
226
- 'data-group-id': mergedGroupId,
227
- ...hasRunnerDump ? {
228
- 'data-report-id': mergedGroupId
229
- } : {},
230
- [DATA_SCREENSHOT_MODE_ATTR]: hasDirectoryModeReport ? 'directory' : 'inline',
231
- playwright_test_duration: reportAttributes.testDuration,
232
- playwright_test_status: reportAttributes.testStatus,
233
- playwright_test_title: reportAttributes.testTitle,
234
- playwright_test_id: reportAttributes.testId,
235
- playwright_test_description: reportAttributes.testDescription,
236
- is_merged: true
237
- }
238
- }, void 0, void 0, false)}\n`;
239
- appendFileSync(outputFilePath, reportHtmlStr);
240
267
  }
268
+ let runner;
241
269
  if (hasRunnerDump) {
242
270
  const executions = dedupeExecutionsKeepLatest(agentReports.flatMap((report)=>report.executions));
243
271
  const metrics = collectTestRunReportMetrics({
@@ -253,30 +281,22 @@ class ReportMergingTool {
253
281
  })
254
282
  ]
255
283
  ] : []));
256
- appendFileSync(outputFilePath, generateTestRunReportScriptTag(mergeTestRunReportDumps(runnerSources, metrics, uuid(), executionMetrics)));
284
+ runner = mergeTestRunReportDumps(runnerSources, metrics, uuid(), executionMetrics);
257
285
  }
258
- const agentComment = mergedAgentReportComment(agentReports);
259
- if (agentComment) appendFileSync(outputFilePath, agentComment);
260
- appendFileSync(outputFilePath, `${htmlEndTag}\n`);
261
- logMsg(`Successfully merged new report: ${outputFilePath}`);
286
+ writer.finishSync(runner, mergedAgentReportComment(agentReports));
287
+ publishReportArtifactSync(external ? temporary : entry, external ? artifact : outputFilePath, external, overwrite);
262
288
  if (rmOriginalReports) {
263
- let removedReportCount = 0;
264
- let reportCountWithFilePath = 0;
265
- for (const info of this.reportInfos)if (info.reportFilePath) {
266
- reportCountWithFilePath += 1;
267
- try {
268
- removeReportArtifact(info.reportFilePath);
269
- removedReportCount += 1;
270
- } catch (error) {
271
- logMsg(`Error deleting report ${info.reportFilePath}: ${error}`);
272
- }
273
- }
274
- logMsg(`Removed ${removedReportCount}/${reportCountWithFilePath} original reports`);
289
+ for (const sourcePath of new Set(this.reportInfos.flatMap((info)=>info.reportFilePath ? [
290
+ resolve(info.reportFilePath)
291
+ ] : [])))if (sourcePath !== outputFilePath) removeReportArtifact(sourcePath);
275
292
  }
293
+ logMsg(`Successfully merged new report: ${outputFilePath}`);
276
294
  return outputFilePath;
277
- } catch (error) {
278
- logMsg(`Error in mergeReports: ${error}`);
279
- throw error;
295
+ } finally{
296
+ rmSync(temporary, {
297
+ recursive: true,
298
+ force: true
299
+ });
280
300
  }
281
301
  }
282
302
  constructor(){
@@ -351,8 +371,15 @@ const prepareTestRunReportSources = (sources, loadedDumps)=>{
351
371
  continue;
352
372
  }
353
373
  const sourceKey = source.sourcePath;
374
+ const imageOrigins = new Map();
375
+ JSON.parse(baseDump.serialize(), (_key, value)=>{
376
+ const ref = normalizeStoredImageRef(value);
377
+ if (ref) imageOrigins.set(ref.id, 'file' === ref.storage ? source.dumpPath ?? source.sourcePath : source.sourcePath);
378
+ return value;
379
+ });
354
380
  const shared = preparedByPath.get(sourceKey);
355
381
  if (shared) {
382
+ for (const [id, origin] of imageOrigins)shared.imageOrigins.set(id, origin);
356
383
  if (source.dumpPath) shared.dump.executions = dedupeExecutionsKeepLatest([
357
384
  ...shared.dump.executions,
358
385
  ...baseDump.executions
@@ -412,6 +439,7 @@ const prepareTestRunReportSources = (sources, loadedDumps)=>{
412
439
  executionIds,
413
440
  collection: source.collection,
414
441
  dump,
442
+ imageOrigins,
415
443
  ...source.dumpPath ? {
416
444
  dumpPath: source.dumpPath
417
445
  } : {}
@@ -486,41 +514,25 @@ class TestRunReportAssembler {
486
514
  });
487
515
  const artifact = join(outputDir, options.reportFileName);
488
516
  const finalPath = directoryMode ? join(artifact, 'index.html') : `${artifact}.html`;
489
- if (!options.overwrite && (existsSync(artifact) || existsSync(`${artifact}.html`))) throw new Error(`Report artifact already exists: ${artifact}`);
517
+ if (!options.overwrite && existsSync(directoryMode ? artifact : finalPath)) throw new Error(`Report artifact already exists: ${artifact}`);
490
518
  const temporary = await mkdtemp(join(outputDir, '.test-run-report-'));
491
519
  const htmlPath = join(temporary, 'index.html');
492
520
  try {
493
- await writeFile(htmlPath, getReportTpl());
494
- if (directoryMode) await appendFile(htmlPath, getBaseUrlFixScript());
495
- const images = new TestReportImages(htmlPath, directoryMode);
521
+ const writer = new ReportArtifactWriter(htmlPath, directoryMode, 'test', dump.runId);
522
+ await writer.initialize();
496
523
  const reports = [];
497
524
  for (const source of new Map(prepared.sources.map((source)=>[
498
525
  source.reportId,
499
526
  source
500
527
  ])).values()){
501
- const serialized = await images.rewriteAsync(source.dump.serialize(), source.dumpPath ?? source.sourcePath);
502
528
  reports.push(source.dump);
503
- await appendFile(htmlPath, reportHTMLContent({
504
- dumpString: serialized,
505
- attributes: {
506
- 'data-group-id': source.reportId,
507
- 'data-report-id': source.reportId,
508
- 'data-runner-scope-id': source.scopeId,
509
- [DATA_SCREENSHOT_MODE_ATTR]: directoryMode ? 'directory' : 'inline'
510
- }
511
- }, void 0, void 0, false));
512
- }
513
- await appendFile(htmlPath, generateTestRunReportScriptTag(dump) + mergedAgentReportComment(reports));
514
- if (options.overwrite) {
515
- await rm(artifact, {
516
- recursive: true,
517
- force: true
518
- });
519
- await rm(`${artifact}.html`, {
520
- force: true
521
- });
529
+ await writer.addGroup(source.reportId, source.dump.serialize(), source.dumpPath ?? source.sourcePath, {
530
+ 'data-report-id': source.reportId,
531
+ 'data-runner-scope-id': source.scopeId
532
+ }, source.imageOrigins);
522
533
  }
523
- await rename(directoryMode ? temporary : htmlPath, directoryMode ? artifact : finalPath);
534
+ await writer.finish(dump, mergedAgentReportComment(reports));
535
+ await publishReportArtifact(directoryMode ? temporary : htmlPath, directoryMode ? artifact : finalPath, directoryMode, options.overwrite);
524
536
  return finalPath;
525
537
  } finally{
526
538
  await rm(temporary, {
@@ -558,8 +570,7 @@ class TestRunReportAssembler {
558
570
  const standaloneReportPath = `${finalArtifactPath}.html`;
559
571
  const finalReportPath = directoryMode ? directoryReportPath : standaloneReportPath;
560
572
  const existingArtifacts = [
561
- finalArtifactPath,
562
- standaloneReportPath
573
+ directoryMode ? finalArtifactPath : standaloneReportPath
563
574
  ].filter(existsSync);
564
575
  if (existingArtifacts.length > 0 && !options.overwrite) throw new Error(`Report artifact already exists: ${existingArtifacts[0]}\nSet overwrite to true to overwrite this report.`);
565
576
  const temporaryRoot = mkdtempSync(join(outputDir, '.test-run-report-'));
@@ -569,39 +580,21 @@ class TestRunReportAssembler {
569
580
  if (directoryMode) mkdirSync(temporaryArtifactPath, {
570
581
  recursive: true
571
582
  });
572
- const htmlEndTag = '</html>';
573
- const template = getReportTpl();
574
- const htmlEndIndex = template.lastIndexOf(htmlEndTag);
575
- appendFileSync(temporaryReportPath, -1 === htmlEndIndex ? template : template.slice(0, htmlEndIndex));
576
- if (directoryMode) appendFileSync(temporaryReportPath, getBaseUrlFixScript());
577
- const images = new TestReportImages(temporaryReportPath, directoryMode);
583
+ const writer = new ReportArtifactWriter(temporaryReportPath, directoryMode, 'test', runnerDump.runId);
584
+ writer.initializeSync();
578
585
  const agentReports = [];
579
586
  for (const source of new Map(prepared.sources.map((source)=>[
580
587
  source.reportId,
581
588
  source
582
589
  ])).values()){
583
- const serialized = images.rewriteSync(source.dump.serialize(), source.dumpPath ?? source.sourcePath);
584
590
  agentReports.push(source.dump);
585
- appendFileSync(temporaryReportPath, `${reportHTMLContent({
586
- dumpString: serialized,
587
- attributes: {
588
- 'data-group-id': source.reportId,
589
- 'data-report-id': source.reportId,
590
- 'data-runner-scope-id': source.scopeId,
591
- [DATA_SCREENSHOT_MODE_ATTR]: directoryMode ? 'directory' : 'inline'
592
- }
593
- }, void 0, void 0, false)}\n`);
591
+ writer.addGroupSync(source.reportId, source.dump.serialize(), source.dumpPath ?? source.sourcePath, {
592
+ 'data-report-id': source.reportId,
593
+ 'data-runner-scope-id': source.scopeId
594
+ }, source.imageOrigins);
594
595
  }
595
- appendFileSync(temporaryReportPath, generateTestRunReportScriptTag(runnerDump));
596
- const agentComment = mergedAgentReportComment(agentReports);
597
- if (agentComment) appendFileSync(temporaryReportPath, agentComment);
598
- appendFileSync(temporaryReportPath, `${htmlEndTag}\n`);
599
- if (existsSync(finalArtifactPath)) rmSync(finalArtifactPath, {
600
- recursive: true,
601
- force: true
602
- });
603
- if (existsSync(standaloneReportPath)) unlinkSync(standaloneReportPath);
604
- renameSync(directoryMode ? temporaryArtifactPath : temporaryReportPath, directoryMode ? finalArtifactPath : standaloneReportPath);
596
+ writer.finishSync(runnerDump, mergedAgentReportComment(agentReports));
597
+ publishReportArtifactSync(directoryMode ? temporaryArtifactPath : temporaryReportPath, directoryMode ? finalArtifactPath : standaloneReportPath, directoryMode, options.overwrite);
605
598
  rmSync(temporaryRoot, {
606
599
  recursive: true,
607
600
  force: true
@@ -717,6 +710,6 @@ function splitReportHtmlByExecution(options) {
717
710
  screenshotFiles: Array.from(writtenScreenshotFiles).sort().map((fileName)=>join(screenshotsDir, fileName))
718
711
  };
719
712
  }
720
- export { ExecutionRecorder, ReportArtifactStore, ReportMergingTool, TestRunReportAssembler, calculateTestRunHealth, classifyTestRunCase, collectDedupedExecutions, dedupeExecutionsKeepLatest, isDirectoryModeReport, readReportActionDump, readReportArtifactManifest, readReportArtifactManifestAsync, readReportDumpScripts, readReportRunnerDump, removeReportArtifact, splitReportHtmlByExecution, streamReportDumpScripts };
713
+ export { ExecutionRecorder, ReportArtifactStore, ReportMergingTool, ReportSession, TestRunReportAssembler, calculateTestRunHealth, classifyTestRunCase, collectDedupedExecutions, dedupeExecutionsKeepLatest, exportReportArtifact, isDirectoryModeReport, readReportActionDump, readReportArtifactManifest, readReportArtifactManifestAsync, readReportDumpScripts, readReportRunnerDump, removeReportArtifact, splitReportHtmlByExecution, streamReportDumpScripts };
721
714
 
722
715
  //# sourceMappingURL=report.mjs.map