executable-stories-e2e 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -25,6 +25,8 @@ npx executable-stories format --format html,markdown # reads .executable-stori
25
25
 
26
26
  Option: `storyReporter({ rawRunPath })` writes somewhere else, relative to the project root.
27
27
 
28
+ Works with e2e 0.15 and later. e2e 0.18 and later need Node.js `^22.22.3 || >=24.8.0`; fields a newer e2e adds are read when present.
29
+
28
30
  ## How a run maps
29
31
 
30
32
  | e2e | executable-stories |
@@ -43,6 +45,10 @@ Option: `storyReporter({ rawRunPath })` writes somewhere else, relative to the p
43
45
  | error `details.expected` / `observed` | expected / actual on the failure |
44
46
  | test tags | story tags |
45
47
  | agent step `explanation` | `Agent:` note under the step |
48
+ | `toHaveScreenshot` expected / actual / diff images | attachments on the step, paired in the report as a comparison |
49
+ | `trace.md` page (e2e 0.19+) | `trace` attachment (markdown) on the test |
50
+ | app log warning / error (e2e 0.19+) | `App <source> <level>:` note under the step that was running |
51
+ | skip reason on a failed attempt (e2e 0.19+) | `Skipped itself after a failure:` note on the scenario |
46
52
 
47
53
  The report shows the final attempt. Unselected results stay out. A `--last-failed` or tag-filtered run is marked `runScope: "filtered"`, so it keeps the scenarios it did not run.
48
54
 
package/dist/index.cjs CHANGED
@@ -313,7 +313,20 @@ function agentExplanation(step) {
313
313
  if (!why || step.error?.message.includes(why)) return [];
314
314
  return [{ kind: "note", text: `Agent: ${why}`, phase: "runtime" }];
315
315
  }
316
- function toSteps(groups) {
316
+ var APP_LOG_SOURCE = {
317
+ console: "console",
318
+ error: "uncaught",
319
+ network: "network",
320
+ system: "system"
321
+ };
322
+ function appLogNotes(lines) {
323
+ return lines.filter((l) => l.level === "error" || l.level === "warning").map((l) => ({
324
+ kind: "note",
325
+ text: `App ${APP_LOG_SOURCE[l.source] ?? l.source} ${l.level}: ${l.text}`,
326
+ phase: "runtime"
327
+ }));
328
+ }
329
+ function toSteps(groups, appLog) {
317
330
  const out = [];
318
331
  groups.forEach((group, index) => {
319
332
  const repeat = group.keyword !== "And" && group.keyword !== "But" && out.some((s) => s.keyword === group.keyword);
@@ -328,7 +341,8 @@ function toSteps(groups) {
328
341
  phase: "runtime"
329
342
  }
330
343
  ] : [],
331
- ...agentExplanation(c)
344
+ ...agentExplanation(c),
345
+ ...appLogNotes(appLog.filter((l) => l.step === c.index))
332
346
  ]);
333
347
  out.push({
334
348
  id: `step-${index}`,
@@ -341,6 +355,10 @@ function toSteps(groups) {
341
355
  });
342
356
  return out;
343
357
  }
358
+ function artifactName(artifact) {
359
+ const file = path2.basename(artifact.path ?? "");
360
+ return /-(?:expected|actual|diff|previous)\.png$/.test(file) ? file : artifact.kind;
361
+ }
344
362
  function toMedia(attempt, groups, artifactsRoot) {
345
363
  const attachments = [];
346
364
  const docs = [];
@@ -357,7 +375,7 @@ function toMedia(attempt, groups, artifactsRoot) {
357
375
  )
358
376
  );
359
377
  attachments.push({
360
- name: artifact.kind,
378
+ name: artifactName(artifact),
361
379
  mediaType: artifact.mediaType,
362
380
  path: path2.join(artifactsRoot, artifact.path),
363
381
  fileName: path2.basename(artifact.path),
@@ -381,7 +399,9 @@ function finalAttempt(result, report) {
381
399
  startedAt: member?.startedAt ?? last.startedAt,
382
400
  durationMs: member?.durationMs ?? last.durationMs,
383
401
  artifacts: last.artifacts,
384
- steps: member?.steps ?? []
402
+ steps: member?.steps ?? [],
403
+ ...member?.appLog ? { appLog: member.appLog } : {},
404
+ ...member?.skip ? { skip: member.skip } : {}
385
405
  },
386
406
  count: attempts.length
387
407
  };
@@ -399,7 +419,7 @@ function markersFor(result, attempt, report, markers, projectRoot) {
399
419
  (m) => m.file === file && m.line >= from && m.line < to && m.at >= start && m.at <= end
400
420
  ).sort((a, b) => a.at - b.at);
401
421
  }
402
- function toTestCase(result, report, artifactsRoot, markers, projectRoot) {
422
+ function toTestCase(result, report, artifactsRoot, markers, projectRoot, tracePage) {
403
423
  const scenario = result.titlePath.at(-1) ?? result.testId;
404
424
  const suitePath = result.titlePath.slice(0, -1);
405
425
  const { attempt, count } = finalAttempt(result, report);
@@ -414,6 +434,23 @@ function toTestCase(result, report, artifactsRoot, markers, projectRoot) {
414
434
  groups,
415
435
  artifactsRoot
416
436
  );
437
+ if (tracePage !== void 0) {
438
+ attachments.push({
439
+ name: "trace",
440
+ mediaType: "text/markdown",
441
+ path: path2.resolve(projectRoot, tracePage),
442
+ fileName: path2.basename(tracePage)
443
+ });
444
+ }
445
+ const appLog = attempt?.appLog ?? [];
446
+ docs.push(...appLogNotes(appLog.filter((l) => l.step === void 0)));
447
+ if (result.status !== "skipped" && attempt?.skip !== void 0) {
448
+ docs.push({
449
+ kind: "note",
450
+ text: `Skipped itself after a failure: ${attempt.skip.reason}`,
451
+ phase: "runtime"
452
+ });
453
+ }
417
454
  const error = attempt?.error;
418
455
  const details = error?.details;
419
456
  const stepEvents = groups.map((group, index) => {
@@ -433,7 +470,7 @@ function toTestCase(result, report, artifactsRoot, markers, projectRoot) {
433
470
  titlePath: [...result.titlePath],
434
471
  story: {
435
472
  scenario,
436
- steps: toSteps(groups),
473
+ steps: toSteps(groups, appLog),
437
474
  ...result.tags.length > 0 ? { tags: [...result.tags] } : {},
438
475
  ...suitePath.length > 0 ? { suitePath } : {},
439
476
  ...docs.length > 0 ? { docs } : {}
@@ -461,7 +498,14 @@ function toRawRun(args) {
461
498
  const filtered = args.lastRun !== void 0 || results.length < report.run.results.length;
462
499
  return {
463
500
  testCases: results.map(
464
- (r) => toTestCase(r, report, artifactsRoot, args.markers ?? [], projectRoot)
501
+ (r) => toTestCase(
502
+ r,
503
+ report,
504
+ artifactsRoot,
505
+ args.markers ?? [],
506
+ projectRoot,
507
+ args.traces?.get(r.id)
508
+ )
465
509
  ),
466
510
  startedAtMs: Date.parse(report.run.startedAt),
467
511
  finishedAtMs: Date.parse(report.run.finishedAt),
@@ -505,11 +549,13 @@ function storyReporter(options = {}) {
505
549
  await (0, import_promises.rm)(markersDir, { recursive: true, force: true });
506
550
  return [{ label: "Stories", text: "skipped (explore run)" }];
507
551
  }
552
+ const traces = run.traces;
508
553
  const rawRun = toRawRun({
509
554
  report: run.report,
510
555
  projectRoot: run.projectRoot,
511
556
  artifactsRoot: run.artifactsRoot,
512
557
  markers: await readMarkers(markersDir),
558
+ ...traces ? { traces } : {},
513
559
  ...run.lastRun ? { lastRun: run.lastRun } : {}
514
560
  });
515
561
  await (0, import_promises.rm)(markersDir, { recursive: true, force: true });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/phrase.ts","../src/story.ts"],"sourcesContent":["/**\n * executable-stories reporter for the `e2e` agentic test runner.\n *\n * The reporter reads the finished `report-1` document and writes the RawRun\n * JSON every other adapter writes. `executable-stories format` takes it from\n * there. Optional `story.given/when/then` markers in a test name the steps in\n * the test's own words; without them, step text is phrased from e2e's record.\n */\n\nimport { rmSync } from 'node:fs';\nimport { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { FinishedRun, Report, Reporter, ReporterSummary } from 'e2e';\nimport type {\n RawAttachment,\n RawRun,\n RawStatus,\n RawStepEvent,\n RawTestCase,\n} from 'executable-stories-core/types/raw';\nimport type {\n DocEntry,\n StepKeyword,\n StoryStep,\n} from 'executable-stories-core/types/story';\nimport { phraseStep } from './phrase';\nimport { MARKERS_DIR, type Marker } from './story';\n\nexport { story, type Marker, type MarkerKeyword } from './story';\nexport { phraseStep } from './phrase';\n\ntype ReportResult = Report['run']['results'][number];\ntype ReportAttempt = ReportResult['attempts'][number];\ntype ReportStep = ReportAttempt['steps'][number];\ntype ReportArtifact = ReportAttempt['artifacts'][number];\ntype ReportError = NonNullable<ReportAttempt['error']>;\n\nexport interface StoryReporterOptions {\n /** Where to write the RawRun JSON, relative to the project root. Default: `.executable-stories/raw-run.json`, where `executable-stories format` looks first. */\n rawRunPath?: string;\n}\n\n/**\n * Exhaustive over e2e's unions at compile time, forgiving at run time: a newer\n * e2e may report a kind or status this version has never seen, and its\n * contract says to ignore what you do not know rather than fail the reporter.\n */\nfunction unknownValue<T>(_value: never, fallback: T): T {\n return fallback;\n}\n\nfunction resultStatus(status: ReportResult['status']): RawStatus {\n switch (status) {\n case 'passed':\n case 'flaky': // the final attempt passed; `retry` records the failed ones\n return 'pass';\n case 'failed':\n return 'fail';\n case 'skipped':\n return 'skip';\n case 'timed-out':\n return 'timeout';\n case 'interrupted':\n return 'interrupted';\n default:\n return unknownValue(status, 'unknown');\n }\n}\n\nfunction stepStatus(status: ReportStep['status']): RawStatus {\n switch (status) {\n case 'passed':\n return 'pass';\n // Blocked and timed-out steps stopped the scenario, so they read as fail;\n // the error message keeps e2e's code.\n case 'failed':\n case 'blocked':\n case 'timed-out':\n return 'fail';\n // A cancelled step did not finish, so it reads as not run.\n case 'cancelled':\n return 'skip';\n default:\n return unknownValue(status, 'unknown');\n }\n}\n\n/** Arrange, act or assert, by what the step did. Repeats become `And` in {@link toSteps}. */\nfunction primaryKeyword(step: ReportStep): 'Given' | 'When' | 'Then' {\n switch (step.kind) {\n case 'app':\n case 'session':\n case 'resource':\n return 'Given';\n case 'locator':\n case 'screen':\n return 'When';\n case 'assertion':\n return 'Then';\n case 'agent':\n return step.api === 'agent.assert' ? 'Then' : 'When';\n default:\n return unknownValue(step.kind, 'When');\n }\n}\n\n/** Agent steps are prose already; the rest are phrased from the API they called on what. */\nfunction stepText(step: ReportStep): string {\n if (step.kind === 'agent' && step.label) return step.label;\n return phraseStep(step.api, step.label);\n}\n\nfunction errorMessage(error: ReportError): string {\n return `${error.code}: ${error.message}`;\n}\n\n/**\n * One rendered step: a marker with the e2e steps that ran under it, or a\n * single e2e step when the test has no markers before it.\n */\ninterface Group {\n keyword: Marker['keyword'];\n text: string;\n marker: boolean;\n children: ReportStep[];\n}\n\nfunction toGroups(\n steps: readonly ReportStep[],\n markers: readonly Marker[],\n projectRoot: string,\n): Group[] {\n const groups: Group[] = [];\n let pending = 0;\n let current: Group | undefined;\n // A marker goes before a step that started after it. Timestamps are whole\n // milliseconds, so on a tie the source position decides: a marker written\n // before the step in the same file, on an earlier line or earlier on the\n // same line, came first. Elsewhere, the marker wins.\n const before = (m: Marker, step: ReportStep | undefined): boolean => {\n if (step === undefined) return true;\n const at = Date.parse(step.startedAt);\n if (m.at !== at) return m.at < at;\n if (m.file !== path.resolve(projectRoot, step.source.file)) return true;\n return (\n m.line < step.source.line ||\n (m.line === step.source.line && (m.column ?? 0) < step.source.column)\n );\n };\n const takeMarkersBefore = (step: ReportStep | undefined): void => {\n while (pending < markers.length && before(markers[pending]!, step)) {\n const m = markers[pending++]!;\n current = {\n keyword: m.keyword,\n text: m.text,\n marker: true,\n children: [],\n };\n groups.push(current);\n }\n };\n for (const step of steps) {\n takeMarkersBefore(step);\n if (current) current.children.push(step);\n else\n groups.push({\n keyword: primaryKeyword(step),\n text: stepText(step),\n marker: false,\n children: [step],\n });\n }\n takeMarkersBefore(undefined);\n return groups;\n}\n\n/** A step that never reached a verdict (blocked, timed out, cancelled) asserted nothing we can see. */\nfunction isObservedClaim(step: ReportStep): boolean {\n return (\n primaryKeyword(step) === 'Then' &&\n (step.status === 'passed' || step.status === 'failed')\n );\n}\n\nfunction groupStatus(group: Group): RawStatus {\n const statuses = group.children.map((c) => stepStatus(c.status));\n for (const worst of ['fail', 'unknown', 'skip'] as const)\n if (statuses.includes(worst)) return worst;\n return 'pass';\n}\n\n/** The agent's explanation, unless the error the report shows already says it. */\nfunction agentExplanation(step: ReportStep): DocEntry[] {\n const why = step.explanation?.trim();\n if (!why || step.error?.message.includes(why)) return [];\n return [{ kind: 'note', text: `Agent: ${why}`, phase: 'runtime' }];\n}\n\nfunction toSteps(groups: readonly Group[]): StoryStep[] {\n const out: StoryStep[] = [];\n groups.forEach((group, index) => {\n const repeat =\n group.keyword !== 'And' &&\n group.keyword !== 'But' &&\n out.some((s) => s.keyword === group.keyword);\n const keyword: StepKeyword = repeat ? 'And' : group.keyword;\n const claims = group.children.filter(isObservedClaim).length;\n // e2e records locator and agent claims as steps. A plain value matcher\n // such as `expect(2).toBe(2)` records none, so a group without a recorded\n // claim leaves the count out.\n const assertions = claims > 0 ? { assertions: claims } : {};\n // A marker lists the e2e steps under it; any step adds the agent's own\n // account of what it did or why it judged a claim true.\n const docs: DocEntry[] = group.children.flatMap((c): DocEntry[] => [\n ...(group.marker\n ? [\n {\n kind: 'note' as const,\n text: stepText(c),\n phase: 'runtime' as const,\n },\n ]\n : []),\n ...agentExplanation(c),\n ]);\n out.push({\n id: `step-${index}`,\n keyword,\n text: group.text,\n durationMs: group.children.reduce((sum, c) => sum + c.durationMs, 0),\n ...assertions,\n ...(docs.length > 0 ? { docs } : {}),\n });\n });\n return out;\n}\n\n/**\n * Artifacts become attachments, the shape the Playwright adapter gives\n * screenshots, videos and traces. A step-produced artifact is pinned to its\n * step. A hosted video has a URL and no file, so it becomes a video doc.\n */\nfunction toMedia(\n attempt: {\n artifacts: readonly ReportArtifact[];\n steps: readonly ReportStep[];\n },\n groups: readonly Group[],\n artifactsRoot: string,\n): { attachments: RawAttachment[]; docs: DocEntry[] } {\n const attachments: RawAttachment[] = [];\n const docs: DocEntry[] = [];\n for (const artifact of attempt.artifacts) {\n if (artifact.path === undefined) {\n if (\n artifact.kind === 'video' &&\n artifact.url !== undefined &&\n /^https?:\\/\\//.test(artifact.url)\n ) {\n docs.push({ kind: 'video', path: artifact.url, phase: 'runtime' });\n }\n continue;\n }\n const stepIndex = groups.findIndex((group) =>\n group.children.some(\n (step) =>\n step.artifacts.includes(artifact.id) ||\n (artifact.producer?.kind === 'step' &&\n artifact.producer.stepId === step.id),\n ),\n );\n attachments.push({\n name: artifact.kind,\n mediaType: artifact.mediaType,\n path: path.join(artifactsRoot, artifact.path),\n fileName: path.basename(artifact.path),\n ...(artifact.size !== undefined ? { byteLength: artifact.size } : {}),\n ...(stepIndex >= 0 ? { stepIndex, stepId: `step-${stepIndex}` } : {}),\n });\n }\n return { attachments, docs };\n}\n\n/** The final attempt, wherever the report keeps it: a serial member's lives on its group. */\nfunction finalAttempt(\n result: ReportResult,\n report: Report,\n): {\n attempt?: Pick<\n ReportAttempt,\n 'error' | 'startedAt' | 'durationMs' | 'artifacts' | 'steps'\n >;\n count: number;\n} {\n if (result.serialGroupId === undefined) {\n return { attempt: result.attempts.at(-1), count: result.attempts.length };\n }\n const attempts =\n report.run.serialGroups.find((g) => g.id === result.serialGroupId)\n ?.attempts ?? [];\n const last = attempts.at(-1);\n const member = last?.members.find((m) => m.testId === result.testId);\n if (last === undefined) return { count: 0 };\n return {\n attempt: {\n error: member?.error ?? last.error,\n startedAt: member?.startedAt ?? last.startedAt,\n durationMs: member?.durationMs ?? last.durationMs,\n artifacts: last.artifacts,\n steps: member?.steps ?? [],\n },\n count: attempts.length,\n };\n}\n\n/**\n * The markers one attempt recorded: called from the test's own file, below its\n * declaration and above the next test's, while the attempt ran.\n */\nfunction markersFor(\n result: ReportResult,\n attempt: Pick<ReportAttempt, 'startedAt' | 'durationMs'> | undefined,\n report: Report,\n markers: readonly Marker[],\n projectRoot: string,\n): Marker[] {\n if (attempt === undefined || markers.length === 0) return [];\n const file = path.resolve(projectRoot, result.file);\n const from = result.source.line;\n const to = Math.min(\n ...report.run.results\n .filter((r) => r.file === result.file && r.source.line > from)\n .map((r) => r.source.line),\n );\n const start = Date.parse(attempt.startedAt);\n const end = start + attempt.durationMs + 1000; // slack for clock reads on either side\n return markers\n .filter(\n (m) =>\n m.file === file &&\n m.line >= from &&\n m.line < to &&\n m.at >= start &&\n m.at <= end,\n )\n .sort((a, b) => a.at - b.at);\n}\n\nfunction toTestCase(\n result: ReportResult,\n report: Report,\n artifactsRoot: string,\n markers: readonly Marker[],\n projectRoot: string,\n): RawTestCase {\n const scenario = result.titlePath.at(-1) ?? result.testId;\n const suitePath = result.titlePath.slice(0, -1);\n const { attempt, count } = finalAttempt(result, report);\n const steps = attempt?.steps ?? [];\n const groups = toGroups(\n steps,\n markersFor(result, attempt, report, markers, projectRoot),\n projectRoot,\n );\n const { attachments, docs } = toMedia(\n { artifacts: attempt?.artifacts ?? [], steps },\n groups,\n artifactsRoot,\n );\n const error = attempt?.error;\n const details = error?.details;\n const stepEvents: RawStepEvent[] = groups.map((group, index) => {\n const failed = group.children.find((c) => c.error !== undefined);\n return {\n index,\n stepId: `step-${index}`,\n title: group.text,\n status: groupStatus(group),\n durationMs: group.children.reduce((sum, c) => sum + c.durationMs, 0),\n ...(failed?.error ? { errorMessage: errorMessage(failed.error) } : {}),\n };\n });\n\n return {\n externalId: result.id,\n title: scenario,\n titlePath: [...result.titlePath],\n story: {\n scenario,\n steps: toSteps(groups),\n ...(result.tags.length > 0 ? { tags: [...result.tags] } : {}),\n ...(suitePath.length > 0 ? { suitePath } : {}),\n ...(docs.length > 0 ? { docs } : {}),\n },\n sourceFile: result.file,\n sourceLine: Math.max(1, result.source.line),\n status: resultStatus(result.status),\n durationMs: attempt?.durationMs ?? 0,\n ...(error\n ? {\n error: {\n message: errorMessage(error),\n ...(details?.expected !== undefined\n ? { expected: details.expected }\n : {}),\n ...(details?.observed !== undefined\n ? { actual: details.observed }\n : {}),\n },\n }\n : {}),\n ...(stepEvents.length > 0 ? { stepEvents } : {}),\n ...(attachments.length > 0 ? { attachments } : {}),\n retry: Math.max(0, count - 1),\n projectName: result.targetId,\n };\n}\n\n/**\n * Map an e2e `report-1` document to a RawRun. Pure: artifact paths resolve\n * against `artifactsRoot` and nothing is read from disk.\n */\nexport function toRawRun(args: {\n report: Report;\n projectRoot: string;\n artifactsRoot: string;\n /** Set when the run was `--last-failed`: it then covered only part of the suite. */\n lastRun?: Report;\n /** Step markers the run's tests recorded with `story.given/when/then`. */\n markers?: readonly Marker[];\n}): RawRun {\n const { report, projectRoot, artifactsRoot } = args;\n // Unselected results stay out of the docs.\n const results = report.run.results.filter((r) => r.selected !== false);\n const filtered =\n args.lastRun !== undefined || results.length < report.run.results.length;\n return {\n testCases: results.map((r) =>\n toTestCase(r, report, artifactsRoot, args.markers ?? [], projectRoot),\n ),\n startedAtMs: Date.parse(report.run.startedAt),\n finishedAtMs: Date.parse(report.run.finishedAt),\n projectRoot,\n // Only \"filtered\" is claimed: e2e can narrow a run in ways the report\n // does not record, so \"full\" (which retires scenarios) is never guessed.\n ...(filtered ? { runScope: 'filtered' as const } : {}),\n coveredSourceFiles: [...new Set(results.map((r) => r.file))].sort(),\n ...(report.run.vcs?.commit ? { gitSha: report.run.vcs.commit } : {}),\n };\n}\n\nasync function readMarkers(dir: string): Promise<Marker[]> {\n const files = await readdir(dir).catch(() => [] as string[]);\n const markers: Marker[] = [];\n for (const file of files.filter((f) => f.endsWith('.jsonl'))) {\n for (const line of (await readFile(path.join(dir, file), 'utf8')).split(\n '\\n',\n )) {\n if (!line) continue;\n try {\n markers.push(JSON.parse(line) as Marker);\n } catch {\n // a worker killed mid-write leaves a torn last line; skip it\n }\n }\n }\n return markers;\n}\n\n/** An e2e reporter that writes the run as RawRun JSON for the executable-stories CLI. */\nexport function storyReporter(options: StoryReporterOptions = {}): Reporter {\n return {\n name: 'executable-stories',\n onEvent(event) {\n // Start each run with an empty markers folder. The delete is small and\n // synchronous, which suits onEvent.\n if (event.type === 'run-started')\n rmSync(path.resolve(event.projectRoot, MARKERS_DIR), {\n recursive: true,\n force: true,\n });\n },\n async onRunFinished(run: FinishedRun): Promise<ReporterSummary> {\n const markersDir = path.resolve(run.projectRoot, MARKERS_DIR);\n // An explore run records findings, which the bug-bash skill turns into\n // scenarios. The last test run's raw-run.json stays as it is.\n if (run.report.run.explore !== undefined) {\n await rm(markersDir, { recursive: true, force: true });\n return [{ label: 'Stories', text: 'skipped (explore run)' }];\n }\n const rawRun = toRawRun({\n report: run.report,\n projectRoot: run.projectRoot,\n artifactsRoot: run.artifactsRoot,\n markers: await readMarkers(markersDir),\n ...(run.lastRun ? { lastRun: run.lastRun } : {}),\n });\n await rm(markersDir, { recursive: true, force: true });\n // A run that reached no test keeps the last test run's raw-run.json.\n if (rawRun.testCases.length === 0) {\n return [{ label: 'Stories', text: 'skipped (no tests ran)' }];\n }\n const target = path.resolve(\n run.projectRoot,\n options.rawRunPath ?? '.executable-stories/raw-run.json',\n );\n await mkdir(path.dirname(target), { recursive: true });\n await writeFile(\n target,\n JSON.stringify({ schemaVersion: 1, ...rawRun }, null, 2),\n 'utf8',\n );\n return [\n { label: 'Stories', text: path.relative(run.projectRoot, target) },\n ];\n },\n };\n}\n","/**\n * Plain-language step text for e2e's non-agent steps.\n *\n * e2e records a step as `api` + `label`, where the label is the locator's own\n * description (`getByRole(\"button\", name: \"Increment\")`). This module\n * rephrases the common shapes for a living-docs page. Other shapes keep the\n * raw `api label`.\n */\n\nconst ROLE_NOUNS: Record<string, string> = {\n textbox: 'field',\n searchbox: 'search field',\n combobox: 'dropdown',\n spinbutton: 'number field',\n img: 'image',\n};\n\n/** `getByKind(\"value\"[, name: \"name\"])` with plain string arguments only; scopes, filters, regexes and nth() stay raw. */\nconst SIMPLE_QUERY =\n /^getBy(\\w+)\\((\"(?:[^\"\\\\]|\\\\.)*\")(?:, name: (\"(?:[^\"\\\\]|\\\\.)*\"))?\\)$/;\n\nfunction unquote(json: string): string {\n try {\n return JSON.parse(json) as string;\n } catch {\n return json;\n }\n}\n\n/** A locator description as a noun phrase, or `undefined` when it is not a simple query. */\nexport function describeTarget(label: string): string | undefined {\n const match = SIMPLE_QUERY.exec(label);\n if (!match) return undefined;\n const [, kind, rawValue, rawName] = match;\n const value = unquote(rawValue!);\n const name = rawName === undefined ? undefined : unquote(rawName);\n switch (kind) {\n case 'Role': {\n const noun = ROLE_NOUNS[value] ?? value;\n return name === undefined ? `the ${noun}` : `the \"${name}\" ${noun}`;\n }\n case 'Text':\n return `the text \"${value}\"`;\n case 'Label':\n case 'Placeholder':\n return `the \"${value}\" field`;\n case 'AltText':\n return `the \"${value}\" image`;\n case 'TestId':\n case 'Title':\n return `the \"${value}\" element`;\n default:\n return undefined;\n }\n}\n\nconst ACTIONS: Record<string, string> = {\n click: 'click',\n tap: 'tap',\n dblclick: 'double-click',\n hover: 'hover over',\n check: 'check',\n uncheck: 'uncheck',\n fill: 'fill in',\n clear: 'clear',\n press: 'press a key on',\n pressSequentially: 'type into',\n selectOption: 'choose an option in',\n longPress: 'long-press',\n swipe: 'swipe',\n focus: 'focus',\n};\n\n/** `expect.toX` → what it claims about the target. */\nconst CLAIMS: Record<string, [positive: string, negative: string]> = {\n toBeVisible: ['is visible', 'is not visible'],\n toBeHidden: ['is hidden', 'is not hidden'],\n toBeEnabled: ['is enabled', 'is not enabled'],\n toBeDisabled: ['is disabled', 'is not disabled'],\n toBeChecked: ['is checked', 'is not checked'],\n toBeFocused: ['is focused', 'is not focused'],\n toBeSelected: ['is selected', 'is not selected'],\n toBeExpanded: ['is expanded', 'is not expanded'],\n toBeAttached: ['is on the page', 'is not on the page'],\n toHaveText: ['shows the expected text', 'does not show the given text'],\n toContainText: [\n 'contains the expected text',\n 'does not contain the given text',\n ],\n toHaveValue: ['has the expected value', 'does not have the given value'],\n toHaveCount: [\n 'appears the expected number of times',\n 'does not appear the given number of times',\n ],\n toHaveAccessibleName: [\n 'has the expected accessible name',\n 'does not have the given accessible name',\n ],\n toHaveAttribute: [\n 'has the expected attribute',\n 'does not have the given attribute',\n ],\n};\n\nconst APP: Record<string, string> = {\n 'app.back': 'go back',\n 'app.restart': 'restart the app',\n 'app.clearState': 'clear the app state',\n 'session.launch': 'launch the app session',\n 'session.close': 'close the app session',\n};\n\n/** Step text for one non-agent step. */\nexport function phraseStep(api: string, label: string): string {\n const raw = label ? `${api} ${label}` : api;\n\n if (api === 'app.open') return label ? `open ${label}` : 'open the app';\n if (api === 'app.screenshot')\n return label ? `take a screenshot: ${label}` : 'take a screenshot';\n if (api === 'session.save')\n return label ? `save the session as ${label}` : 'save the session';\n if (APP[api]) return APP[api];\n\n const action = /^locator\\.(\\w+)$/.exec(api)?.[1];\n if (action !== undefined) {\n if (action === 'waitFor') {\n // `<target> → <state>`\n const [target, state] = label.split(' → ');\n const noun = target === undefined ? undefined : describeTarget(target);\n return noun && state ? `wait until ${noun} is ${state}` : raw;\n }\n const verb = ACTIONS[action];\n const noun = describeTarget(label);\n return verb && noun ? `${verb} ${noun}` : raw;\n }\n\n const claim = /^expect\\.(not\\.)?(\\w+)$/.exec(api);\n if (claim) {\n const words = CLAIMS[claim[2]!];\n const noun = describeTarget(label);\n return words && noun ? `${noun} ${words[claim[1] ? 1 : 0]}` : raw;\n }\n\n return raw;\n}\n","/**\n * Step markers for e2e tests.\n *\n * A marker records where and when the test called it. The reporter groups\n * the e2e steps that ran after a marker under the marker's text:\n *\n * ```ts\n * test('incrementing shows the new count', async ({ app, screen }) => {\n * story.given('the counter page is open');\n * await app.open('/');\n * story.when('the user presses Increment twice');\n * await screen.getByRole('button', { name: 'Increment' }).click();\n * await screen.getByRole('button', { name: 'Increment' }).click();\n * story.then('the count reads 2');\n * await expect(screen.getByRole('status')).toHaveText('2');\n * });\n * ```\n */\n\nimport { appendFileSync, mkdirSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport type MarkerKeyword = 'Given' | 'When' | 'Then' | 'And' | 'But';\n\n/** One line of a markers file. */\nexport interface Marker {\n keyword: MarkerKeyword;\n text: string;\n /** Absolute path of the test file that called the marker. */\n file: string;\n line: number;\n /** Orders a marker before a step on its own line; absent reads as the line's start. */\n column?: number;\n /** Epoch milliseconds. */\n at: number;\n}\n\n/** Workers and the reporter both resolve this against the project root e2e runs from. */\nexport const MARKERS_DIR = '.executable-stories/e2e-markers';\n\nconst FRAME = /\\(?((?:file:\\/\\/)?[^\\s()]+?):(\\d+):(\\d+)\\)?$/;\n\n/** The first stack frame outside this module: the test line that called the marker. */\nfunction callSite():\n { file: string; line: number; column: number } | undefined {\n const frames = (new Error().stack ?? '').split('\\n').slice(1);\n let own: string | undefined;\n for (const frame of frames) {\n const match = FRAME.exec(frame.trim());\n if (!match) continue;\n const file = match[1]!.startsWith('file://')\n ? fileURLToPath(match[1]!)\n : match[1]!;\n own ??= file;\n if (file !== own)\n return { file, line: Number(match[2]), column: Number(match[3]) };\n }\n return undefined;\n}\n\nfunction mark(keyword: MarkerKeyword, text: string): void {\n const site = callSite();\n if (site === undefined) return;\n const marker: Marker = { keyword, text, ...site, at: Date.now() };\n const dir = path.resolve(MARKERS_DIR);\n mkdirSync(dir, { recursive: true });\n // One file per worker process; appends from one process stay in order.\n appendFileSync(\n path.join(dir, `${process.pid}.jsonl`),\n `${JSON.stringify(marker)}\\n`,\n );\n}\n\n/** Step markers. Call before the e2e steps the marker describes; they render under its text. */\nexport const story = {\n given: (text: string): void => mark('Given', text),\n when: (text: string): void => mark('When', text),\n then: (text: string): void => mark('Then', text),\n and: (text: string): void => mark('And', text),\n but: (text: string): void => mark('But', text),\n};\n"],"mappings":"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","names":["import_node_fs","path","noun"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/phrase.ts","../src/story.ts"],"sourcesContent":["/**\n * executable-stories reporter for the `e2e` agentic test runner.\n *\n * The reporter reads the finished `report-1` document and writes the RawRun\n * JSON every other adapter writes. `executable-stories format` takes it from\n * there. Optional `story.given/when/then` markers in a test name the steps in\n * the test's own words; without them, step text is phrased from e2e's record.\n */\n\nimport { rmSync } from 'node:fs';\nimport { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { FinishedRun, Report, Reporter, ReporterSummary } from 'e2e';\nimport type {\n RawAttachment,\n RawRun,\n RawStatus,\n RawStepEvent,\n RawTestCase,\n} from 'executable-stories-core/types/raw';\nimport type {\n DocEntry,\n StepKeyword,\n StoryStep,\n} from 'executable-stories-core/types/story';\nimport { phraseStep } from './phrase';\nimport { MARKERS_DIR, type Marker } from './story';\n\nexport { story, type Marker, type MarkerKeyword } from './story';\nexport { phraseStep } from './phrase';\n\ntype ReportResult = Report['run']['results'][number];\ntype ReportAttempt = ReportResult['attempts'][number];\ntype ReportStep = ReportAttempt['steps'][number];\ntype ReportArtifact = ReportAttempt['artifacts'][number];\ntype ReportError = NonNullable<ReportAttempt['error']>;\n/**\n * One line of what the app did during an attempt (e2e 0.19+): console output,\n * an uncaught error, a failed request or a 4xx/5xx response, with the index of\n * the step that was running.\n */\ntype AppLogLine = NonNullable<ReportAttempt['appLog']>[number];\n\ntype FinalAttempt = Pick<\n ReportAttempt,\n | 'error'\n | 'startedAt'\n | 'durationMs'\n | 'artifacts'\n | 'steps'\n | 'appLog'\n | 'skip'\n>;\n\nexport interface StoryReporterOptions {\n /** Where to write the RawRun JSON, relative to the project root. Default: `.executable-stories/raw-run.json`, where `executable-stories format` looks first. */\n rawRunPath?: string;\n}\n\n/**\n * Exhaustive over e2e's unions at compile time, forgiving at run time: a newer\n * e2e may report a kind or status this version has never seen, and its\n * contract says to ignore what you do not know rather than fail the reporter.\n */\nfunction unknownValue<T>(_value: never, fallback: T): T {\n return fallback;\n}\n\nfunction resultStatus(status: ReportResult['status']): RawStatus {\n switch (status) {\n case 'passed':\n case 'flaky': // the final attempt passed; `retry` records the failed ones\n return 'pass';\n case 'failed':\n return 'fail';\n case 'skipped':\n return 'skip';\n case 'timed-out':\n return 'timeout';\n case 'interrupted':\n return 'interrupted';\n default:\n return unknownValue(status, 'unknown');\n }\n}\n\nfunction stepStatus(status: ReportStep['status']): RawStatus {\n switch (status) {\n case 'passed':\n return 'pass';\n // Blocked and timed-out steps stopped the scenario, so they read as fail;\n // the error message keeps e2e's code.\n case 'failed':\n case 'blocked':\n case 'timed-out':\n return 'fail';\n // A cancelled step did not finish, so it reads as not run.\n case 'cancelled':\n return 'skip';\n default:\n return unknownValue(status, 'unknown');\n }\n}\n\n/** Arrange, act or assert, by what the step did. Repeats become `And` in {@link toSteps}. */\nfunction primaryKeyword(step: ReportStep): 'Given' | 'When' | 'Then' {\n switch (step.kind) {\n case 'app':\n case 'session':\n case 'resource':\n return 'Given';\n case 'locator':\n case 'screen':\n return 'When';\n case 'assertion':\n return 'Then';\n case 'agent':\n return step.api === 'agent.assert' ? 'Then' : 'When';\n default:\n return unknownValue(step.kind, 'When');\n }\n}\n\n/** Agent steps are prose already; the rest are phrased from the API they called on what. */\nfunction stepText(step: ReportStep): string {\n if (step.kind === 'agent' && step.label) return step.label;\n return phraseStep(step.api, step.label);\n}\n\nfunction errorMessage(error: ReportError): string {\n return `${error.code}: ${error.message}`;\n}\n\n/**\n * One rendered step: a marker with the e2e steps that ran under it, or a\n * single e2e step when the test has no markers before it.\n */\ninterface Group {\n keyword: Marker['keyword'];\n text: string;\n marker: boolean;\n children: ReportStep[];\n}\n\nfunction toGroups(\n steps: readonly ReportStep[],\n markers: readonly Marker[],\n projectRoot: string,\n): Group[] {\n const groups: Group[] = [];\n let pending = 0;\n let current: Group | undefined;\n // A marker goes before a step that started after it. Timestamps are whole\n // milliseconds, so on a tie the source position decides: a marker written\n // before the step in the same file, on an earlier line or earlier on the\n // same line, came first. Elsewhere, the marker wins.\n const before = (m: Marker, step: ReportStep | undefined): boolean => {\n if (step === undefined) return true;\n const at = Date.parse(step.startedAt);\n if (m.at !== at) return m.at < at;\n if (m.file !== path.resolve(projectRoot, step.source.file)) return true;\n return (\n m.line < step.source.line ||\n (m.line === step.source.line && (m.column ?? 0) < step.source.column)\n );\n };\n const takeMarkersBefore = (step: ReportStep | undefined): void => {\n while (pending < markers.length && before(markers[pending]!, step)) {\n const m = markers[pending++]!;\n current = {\n keyword: m.keyword,\n text: m.text,\n marker: true,\n children: [],\n };\n groups.push(current);\n }\n };\n for (const step of steps) {\n takeMarkersBefore(step);\n if (current) current.children.push(step);\n else\n groups.push({\n keyword: primaryKeyword(step),\n text: stepText(step),\n marker: false,\n children: [step],\n });\n }\n takeMarkersBefore(undefined);\n return groups;\n}\n\n/** A step that never reached a verdict (blocked, timed out, cancelled) asserted nothing we can see. */\nfunction isObservedClaim(step: ReportStep): boolean {\n return (\n primaryKeyword(step) === 'Then' &&\n (step.status === 'passed' || step.status === 'failed')\n );\n}\n\nfunction groupStatus(group: Group): RawStatus {\n const statuses = group.children.map((c) => stepStatus(c.status));\n for (const worst of ['fail', 'unknown', 'skip'] as const)\n if (statuses.includes(worst)) return worst;\n return 'pass';\n}\n\n/** The agent's explanation, unless the error the report shows already says it. */\nfunction agentExplanation(step: ReportStep): DocEntry[] {\n const why = step.explanation?.trim();\n if (!why || step.error?.message.includes(why)) return [];\n return [{ kind: 'note', text: `Agent: ${why}`, phase: 'runtime' }];\n}\n\nconst APP_LOG_SOURCE: Readonly<Record<string, string>> = {\n console: 'console',\n error: 'uncaught',\n network: 'network',\n system: 'system',\n};\n\n/** Warnings and errors the app logged while a step ran; info lines are noise in docs. */\nfunction appLogNotes(lines: readonly AppLogLine[]): DocEntry[] {\n return lines\n .filter((l) => l.level === 'error' || l.level === 'warning')\n .map((l) => ({\n kind: 'note' as const,\n text: `App ${APP_LOG_SOURCE[l.source] ?? l.source} ${l.level}: ${l.text}`,\n phase: 'runtime' as const,\n }));\n}\n\nfunction toSteps(\n groups: readonly Group[],\n appLog: readonly AppLogLine[],\n): StoryStep[] {\n const out: StoryStep[] = [];\n groups.forEach((group, index) => {\n const repeat =\n group.keyword !== 'And' &&\n group.keyword !== 'But' &&\n out.some((s) => s.keyword === group.keyword);\n const keyword: StepKeyword = repeat ? 'And' : group.keyword;\n const claims = group.children.filter(isObservedClaim).length;\n // e2e records locator and agent claims as steps. A plain value matcher\n // such as `expect(2).toBe(2)` records none, so a group without a recorded\n // claim leaves the count out.\n const assertions = claims > 0 ? { assertions: claims } : {};\n // A marker lists the e2e steps under it; any step adds the agent's own\n // account of what it did or why it judged a claim true.\n const docs: DocEntry[] = group.children.flatMap((c): DocEntry[] => [\n ...(group.marker\n ? [\n {\n kind: 'note' as const,\n text: stepText(c),\n phase: 'runtime' as const,\n },\n ]\n : []),\n ...agentExplanation(c),\n ...appLogNotes(appLog.filter((l) => l.step === c.index)),\n ]);\n out.push({\n id: `step-${index}`,\n keyword,\n text: group.text,\n durationMs: group.children.reduce((sum, c) => sum + c.durationMs, 0),\n ...assertions,\n ...(docs.length > 0 ? { docs } : {}),\n });\n });\n return out;\n}\n\n/**\n * A failed `toHaveScreenshot` attaches `<stem>-expected.png`, `-actual.png`\n * and `-diff.png` to its step, all of kind `screenshot`. They keep their file\n * name, as Playwright names the same trio, so the report pairs them.\n */\nfunction artifactName(artifact: ReportArtifact): string {\n const file = path.basename(artifact.path ?? '');\n return /-(?:expected|actual|diff|previous)\\.png$/.test(file)\n ? file\n : artifact.kind;\n}\n\n/**\n * Artifacts become attachments, the shape the Playwright adapter gives\n * screenshots, videos and traces. A step-produced artifact is pinned to its\n * step. A hosted video has a URL and no file, so it becomes a video doc.\n */\nfunction toMedia(\n attempt: {\n artifacts: readonly ReportArtifact[];\n steps: readonly ReportStep[];\n },\n groups: readonly Group[],\n artifactsRoot: string,\n): { attachments: RawAttachment[]; docs: DocEntry[] } {\n const attachments: RawAttachment[] = [];\n const docs: DocEntry[] = [];\n for (const artifact of attempt.artifacts) {\n if (artifact.path === undefined) {\n if (\n artifact.kind === 'video' &&\n artifact.url !== undefined &&\n /^https?:\\/\\//.test(artifact.url)\n ) {\n docs.push({ kind: 'video', path: artifact.url, phase: 'runtime' });\n }\n continue;\n }\n const stepIndex = groups.findIndex((group) =>\n group.children.some(\n (step) =>\n step.artifacts.includes(artifact.id) ||\n (artifact.producer?.kind === 'step' &&\n artifact.producer.stepId === step.id),\n ),\n );\n attachments.push({\n name: artifactName(artifact),\n mediaType: artifact.mediaType,\n path: path.join(artifactsRoot, artifact.path),\n fileName: path.basename(artifact.path),\n ...(artifact.size !== undefined ? { byteLength: artifact.size } : {}),\n ...(stepIndex >= 0 ? { stepIndex, stepId: `step-${stepIndex}` } : {}),\n });\n }\n return { attachments, docs };\n}\n\n/** The final attempt, wherever the report keeps it: a serial member's lives on its group. */\nfunction finalAttempt(\n result: ReportResult,\n report: Report,\n): {\n attempt?: FinalAttempt;\n count: number;\n} {\n if (result.serialGroupId === undefined) {\n return { attempt: result.attempts.at(-1), count: result.attempts.length };\n }\n const attempts =\n report.run.serialGroups.find((g) => g.id === result.serialGroupId)\n ?.attempts ?? [];\n const last = attempts.at(-1);\n const member = last?.members.find((m) => m.testId === result.testId);\n if (last === undefined) return { count: 0 };\n return {\n attempt: {\n error: member?.error ?? last.error,\n startedAt: member?.startedAt ?? last.startedAt,\n durationMs: member?.durationMs ?? last.durationMs,\n artifacts: last.artifacts,\n steps: member?.steps ?? [],\n ...(member?.appLog ? { appLog: member.appLog } : {}),\n ...(member?.skip ? { skip: member.skip } : {}),\n },\n count: attempts.length,\n };\n}\n\n/**\n * The markers one attempt recorded: called from the test's own file, below its\n * declaration and above the next test's, while the attempt ran.\n */\nfunction markersFor(\n result: ReportResult,\n attempt: Pick<ReportAttempt, 'startedAt' | 'durationMs'> | undefined,\n report: Report,\n markers: readonly Marker[],\n projectRoot: string,\n): Marker[] {\n if (attempt === undefined || markers.length === 0) return [];\n const file = path.resolve(projectRoot, result.file);\n const from = result.source.line;\n const to = Math.min(\n ...report.run.results\n .filter((r) => r.file === result.file && r.source.line > from)\n .map((r) => r.source.line),\n );\n const start = Date.parse(attempt.startedAt);\n const end = start + attempt.durationMs + 1000; // slack for clock reads on either side\n return markers\n .filter(\n (m) =>\n m.file === file &&\n m.line >= from &&\n m.line < to &&\n m.at >= start &&\n m.at <= end,\n )\n .sort((a, b) => a.at - b.at);\n}\n\nfunction toTestCase(\n result: ReportResult,\n report: Report,\n artifactsRoot: string,\n markers: readonly Marker[],\n projectRoot: string,\n tracePage: string | undefined,\n): RawTestCase {\n const scenario = result.titlePath.at(-1) ?? result.testId;\n const suitePath = result.titlePath.slice(0, -1);\n const { attempt, count } = finalAttempt(result, report);\n const steps = attempt?.steps ?? [];\n const groups = toGroups(\n steps,\n markersFor(result, attempt, report, markers, projectRoot),\n projectRoot,\n );\n const { attachments, docs } = toMedia(\n { artifacts: attempt?.artifacts ?? [], steps },\n groups,\n artifactsRoot,\n );\n // e2e 0.19 writes a markdown trace page per test that kept one (failed\n // tests by default): what each step did, read and logged.\n if (tracePage !== undefined) {\n attachments.push({\n name: 'trace',\n mediaType: 'text/markdown',\n path: path.resolve(projectRoot, tracePage),\n fileName: path.basename(tracePage),\n });\n }\n const appLog = attempt?.appLog ?? [];\n // Lines logged before the first step have no step to sit under.\n docs.push(...appLogNotes(appLog.filter((l) => l.step === undefined)));\n if (result.status !== 'skipped' && attempt?.skip !== undefined) {\n docs.push({\n kind: 'note',\n text: `Skipped itself after a failure: ${attempt.skip.reason}`,\n phase: 'runtime',\n });\n }\n const error = attempt?.error;\n const details = error?.details;\n const stepEvents: RawStepEvent[] = groups.map((group, index) => {\n const failed = group.children.find((c) => c.error !== undefined);\n return {\n index,\n stepId: `step-${index}`,\n title: group.text,\n status: groupStatus(group),\n durationMs: group.children.reduce((sum, c) => sum + c.durationMs, 0),\n ...(failed?.error ? { errorMessage: errorMessage(failed.error) } : {}),\n };\n });\n\n return {\n externalId: result.id,\n title: scenario,\n titlePath: [...result.titlePath],\n story: {\n scenario,\n steps: toSteps(groups, appLog),\n ...(result.tags.length > 0 ? { tags: [...result.tags] } : {}),\n ...(suitePath.length > 0 ? { suitePath } : {}),\n ...(docs.length > 0 ? { docs } : {}),\n },\n sourceFile: result.file,\n sourceLine: Math.max(1, result.source.line),\n status: resultStatus(result.status),\n durationMs: attempt?.durationMs ?? 0,\n ...(error\n ? {\n error: {\n message: errorMessage(error),\n ...(details?.expected !== undefined\n ? { expected: details.expected }\n : {}),\n ...(details?.observed !== undefined\n ? { actual: details.observed }\n : {}),\n },\n }\n : {}),\n ...(stepEvents.length > 0 ? { stepEvents } : {}),\n ...(attachments.length > 0 ? { attachments } : {}),\n retry: Math.max(0, count - 1),\n projectName: result.targetId,\n };\n}\n\n/**\n * Map an e2e `report-1` document to a RawRun. Pure: artifact paths resolve\n * against `artifactsRoot` and nothing is read from disk.\n */\nexport function toRawRun(args: {\n report: Report;\n projectRoot: string;\n artifactsRoot: string;\n /** Set when the run was `--last-failed`: it then covered only part of the suite. */\n lastRun?: Report;\n /** Step markers the run's tests recorded with `story.given/when/then`. */\n markers?: readonly Marker[];\n /** e2e 0.19+: each kept `trace.md` page by result id, as a path from the project root. */\n traces?: ReadonlyMap<string, string>;\n}): RawRun {\n const { report, projectRoot, artifactsRoot } = args;\n // Unselected results stay out of the docs.\n const results = report.run.results.filter((r) => r.selected !== false);\n const filtered =\n args.lastRun !== undefined || results.length < report.run.results.length;\n return {\n testCases: results.map((r) =>\n toTestCase(\n r,\n report,\n artifactsRoot,\n args.markers ?? [],\n projectRoot,\n args.traces?.get(r.id),\n ),\n ),\n startedAtMs: Date.parse(report.run.startedAt),\n finishedAtMs: Date.parse(report.run.finishedAt),\n projectRoot,\n // Only \"filtered\" is claimed: e2e can narrow a run in ways the report\n // does not record, so \"full\" (which retires scenarios) is never guessed.\n ...(filtered ? { runScope: 'filtered' as const } : {}),\n coveredSourceFiles: [...new Set(results.map((r) => r.file))].sort(),\n ...(report.run.vcs?.commit ? { gitSha: report.run.vcs.commit } : {}),\n };\n}\n\nasync function readMarkers(dir: string): Promise<Marker[]> {\n const files = await readdir(dir).catch(() => [] as string[]);\n const markers: Marker[] = [];\n for (const file of files.filter((f) => f.endsWith('.jsonl'))) {\n for (const line of (await readFile(path.join(dir, file), 'utf8')).split(\n '\\n',\n )) {\n if (!line) continue;\n try {\n markers.push(JSON.parse(line) as Marker);\n } catch {\n // a worker killed mid-write leaves a torn last line; skip it\n }\n }\n }\n return markers;\n}\n\n/** An e2e reporter that writes the run as RawRun JSON for the executable-stories CLI. */\nexport function storyReporter(options: StoryReporterOptions = {}): Reporter {\n return {\n name: 'executable-stories',\n onEvent(event) {\n // Start each run with an empty markers folder. The delete is small and\n // synchronous, which suits onEvent.\n if (event.type === 'run-started')\n rmSync(path.resolve(event.projectRoot, MARKERS_DIR), {\n recursive: true,\n force: true,\n });\n },\n async onRunFinished(run: FinishedRun): Promise<ReporterSummary> {\n const markersDir = path.resolve(run.projectRoot, MARKERS_DIR);\n // An explore run records findings, which the bug-bash skill turns into\n // scenarios. The last test run's raw-run.json stays as it is.\n if (run.report.run.explore !== undefined) {\n await rm(markersDir, { recursive: true, force: true });\n return [{ label: 'Stories', text: 'skipped (explore run)' }];\n }\n // Added in e2e 0.19; undefined on an older runner.\n const traces = run.traces as FinishedRun['traces'] | undefined;\n const rawRun = toRawRun({\n report: run.report,\n projectRoot: run.projectRoot,\n artifactsRoot: run.artifactsRoot,\n markers: await readMarkers(markersDir),\n ...(traces ? { traces } : {}),\n ...(run.lastRun ? { lastRun: run.lastRun } : {}),\n });\n await rm(markersDir, { recursive: true, force: true });\n // A run that reached no test keeps the last test run's raw-run.json.\n if (rawRun.testCases.length === 0) {\n return [{ label: 'Stories', text: 'skipped (no tests ran)' }];\n }\n const target = path.resolve(\n run.projectRoot,\n options.rawRunPath ?? '.executable-stories/raw-run.json',\n );\n await mkdir(path.dirname(target), { recursive: true });\n await writeFile(\n target,\n JSON.stringify({ schemaVersion: 1, ...rawRun }, null, 2),\n 'utf8',\n );\n return [\n { label: 'Stories', text: path.relative(run.projectRoot, target) },\n ];\n },\n };\n}\n","/**\n * Plain-language step text for e2e's non-agent steps.\n *\n * e2e records a step as `api` + `label`, where the label is the locator's own\n * description (`getByRole(\"button\", name: \"Increment\")`). This module\n * rephrases the common shapes for a living-docs page. Other shapes keep the\n * raw `api label`.\n */\n\nconst ROLE_NOUNS: Record<string, string> = {\n textbox: 'field',\n searchbox: 'search field',\n combobox: 'dropdown',\n spinbutton: 'number field',\n img: 'image',\n};\n\n/** `getByKind(\"value\"[, name: \"name\"])` with plain string arguments only; scopes, filters, regexes and nth() stay raw. */\nconst SIMPLE_QUERY =\n /^getBy(\\w+)\\((\"(?:[^\"\\\\]|\\\\.)*\")(?:, name: (\"(?:[^\"\\\\]|\\\\.)*\"))?\\)$/;\n\nfunction unquote(json: string): string {\n try {\n return JSON.parse(json) as string;\n } catch {\n return json;\n }\n}\n\n/** A locator description as a noun phrase, or `undefined` when it is not a simple query. */\nexport function describeTarget(label: string): string | undefined {\n const match = SIMPLE_QUERY.exec(label);\n if (!match) return undefined;\n const [, kind, rawValue, rawName] = match;\n const value = unquote(rawValue!);\n const name = rawName === undefined ? undefined : unquote(rawName);\n switch (kind) {\n case 'Role': {\n const noun = ROLE_NOUNS[value] ?? value;\n return name === undefined ? `the ${noun}` : `the \"${name}\" ${noun}`;\n }\n case 'Text':\n return `the text \"${value}\"`;\n case 'Label':\n case 'Placeholder':\n return `the \"${value}\" field`;\n case 'AltText':\n return `the \"${value}\" image`;\n case 'TestId':\n case 'Title':\n return `the \"${value}\" element`;\n default:\n return undefined;\n }\n}\n\nconst ACTIONS: Record<string, string> = {\n click: 'click',\n tap: 'tap',\n dblclick: 'double-click',\n hover: 'hover over',\n check: 'check',\n uncheck: 'uncheck',\n fill: 'fill in',\n clear: 'clear',\n press: 'press a key on',\n pressSequentially: 'type into',\n selectOption: 'choose an option in',\n longPress: 'long-press',\n swipe: 'swipe',\n focus: 'focus',\n};\n\n/** `expect.toX` → what it claims about the target. */\nconst CLAIMS: Record<string, [positive: string, negative: string]> = {\n toBeVisible: ['is visible', 'is not visible'],\n toBeHidden: ['is hidden', 'is not hidden'],\n toBeEnabled: ['is enabled', 'is not enabled'],\n toBeDisabled: ['is disabled', 'is not disabled'],\n toBeChecked: ['is checked', 'is not checked'],\n toBeFocused: ['is focused', 'is not focused'],\n toBeSelected: ['is selected', 'is not selected'],\n toBeExpanded: ['is expanded', 'is not expanded'],\n toBeAttached: ['is on the page', 'is not on the page'],\n toHaveText: ['shows the expected text', 'does not show the given text'],\n toContainText: [\n 'contains the expected text',\n 'does not contain the given text',\n ],\n toHaveValue: ['has the expected value', 'does not have the given value'],\n toHaveCount: [\n 'appears the expected number of times',\n 'does not appear the given number of times',\n ],\n toHaveAccessibleName: [\n 'has the expected accessible name',\n 'does not have the given accessible name',\n ],\n toHaveAttribute: [\n 'has the expected attribute',\n 'does not have the given attribute',\n ],\n};\n\nconst APP: Record<string, string> = {\n 'app.back': 'go back',\n 'app.restart': 'restart the app',\n 'app.clearState': 'clear the app state',\n 'session.launch': 'launch the app session',\n 'session.close': 'close the app session',\n};\n\n/** Step text for one non-agent step. */\nexport function phraseStep(api: string, label: string): string {\n const raw = label ? `${api} ${label}` : api;\n\n if (api === 'app.open') return label ? `open ${label}` : 'open the app';\n if (api === 'app.screenshot')\n return label ? `take a screenshot: ${label}` : 'take a screenshot';\n if (api === 'session.save')\n return label ? `save the session as ${label}` : 'save the session';\n if (APP[api]) return APP[api];\n\n const action = /^locator\\.(\\w+)$/.exec(api)?.[1];\n if (action !== undefined) {\n if (action === 'waitFor') {\n // `<target> → <state>`\n const [target, state] = label.split(' → ');\n const noun = target === undefined ? undefined : describeTarget(target);\n return noun && state ? `wait until ${noun} is ${state}` : raw;\n }\n const verb = ACTIONS[action];\n const noun = describeTarget(label);\n return verb && noun ? `${verb} ${noun}` : raw;\n }\n\n const claim = /^expect\\.(not\\.)?(\\w+)$/.exec(api);\n if (claim) {\n const words = CLAIMS[claim[2]!];\n const noun = describeTarget(label);\n return words && noun ? `${noun} ${words[claim[1] ? 1 : 0]}` : raw;\n }\n\n return raw;\n}\n","/**\n * Step markers for e2e tests.\n *\n * A marker records where and when the test called it. The reporter groups\n * the e2e steps that ran after a marker under the marker's text:\n *\n * ```ts\n * test('incrementing shows the new count', async ({ app, screen }) => {\n * story.given('the counter page is open');\n * await app.open('/');\n * story.when('the user presses Increment twice');\n * await screen.getByRole('button', { name: 'Increment' }).click();\n * await screen.getByRole('button', { name: 'Increment' }).click();\n * story.then('the count reads 2');\n * await expect(screen.getByRole('status')).toHaveText('2');\n * });\n * ```\n */\n\nimport { appendFileSync, mkdirSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport type MarkerKeyword = 'Given' | 'When' | 'Then' | 'And' | 'But';\n\n/** One line of a markers file. */\nexport interface Marker {\n keyword: MarkerKeyword;\n text: string;\n /** Absolute path of the test file that called the marker. */\n file: string;\n line: number;\n /** Orders a marker before a step on its own line; absent reads as the line's start. */\n column?: number;\n /** Epoch milliseconds. */\n at: number;\n}\n\n/** Workers and the reporter both resolve this against the project root e2e runs from. */\nexport const MARKERS_DIR = '.executable-stories/e2e-markers';\n\nconst FRAME = /\\(?((?:file:\\/\\/)?[^\\s()]+?):(\\d+):(\\d+)\\)?$/;\n\n/** The first stack frame outside this module: the test line that called the marker. */\nfunction callSite():\n { file: string; line: number; column: number } | undefined {\n const frames = (new Error().stack ?? '').split('\\n').slice(1);\n let own: string | undefined;\n for (const frame of frames) {\n const match = FRAME.exec(frame.trim());\n if (!match) continue;\n const file = match[1]!.startsWith('file://')\n ? fileURLToPath(match[1]!)\n : match[1]!;\n own ??= file;\n if (file !== own)\n return { file, line: Number(match[2]), column: Number(match[3]) };\n }\n return undefined;\n}\n\nfunction mark(keyword: MarkerKeyword, text: string): void {\n const site = callSite();\n if (site === undefined) return;\n const marker: Marker = { keyword, text, ...site, at: Date.now() };\n const dir = path.resolve(MARKERS_DIR);\n mkdirSync(dir, { recursive: true });\n // One file per worker process; appends from one process stay in order.\n appendFileSync(\n path.join(dir, `${process.pid}.jsonl`),\n `${JSON.stringify(marker)}\\n`,\n );\n}\n\n/** Step markers. Call before the e2e steps the marker describes; they render under its text. */\nexport const story = {\n given: (text: string): void => mark('Given', text),\n when: (text: string): void => mark('When', text),\n then: (text: string): void => mark('Then', text),\n and: (text: string): void => mark('And', text),\n but: (text: string): void => mark('But', text),\n};\n"],"mappings":"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","names":["import_node_fs","path","noun"]}
package/dist/index.d.cts CHANGED
@@ -69,6 +69,8 @@ declare function toRawRun(args: {
69
69
  lastRun?: Report;
70
70
  /** Step markers the run's tests recorded with `story.given/when/then`. */
71
71
  markers?: readonly Marker[];
72
+ /** e2e 0.19+: each kept `trace.md` page by result id, as a path from the project root. */
73
+ traces?: ReadonlyMap<string, string>;
72
74
  }): RawRun;
73
75
  /** An e2e reporter that writes the run as RawRun JSON for the executable-stories CLI. */
74
76
  declare function storyReporter(options?: StoryReporterOptions): Reporter;
package/dist/index.d.ts CHANGED
@@ -69,6 +69,8 @@ declare function toRawRun(args: {
69
69
  lastRun?: Report;
70
70
  /** Step markers the run's tests recorded with `story.given/when/then`. */
71
71
  markers?: readonly Marker[];
72
+ /** e2e 0.19+: each kept `trace.md` page by result id, as a path from the project root. */
73
+ traces?: ReadonlyMap<string, string>;
72
74
  }): RawRun;
73
75
  /** An e2e reporter that writes the run as RawRun JSON for the executable-stories CLI. */
74
76
  declare function storyReporter(options?: StoryReporterOptions): Reporter;
package/dist/index.js CHANGED
@@ -276,7 +276,20 @@ function agentExplanation(step) {
276
276
  if (!why || step.error?.message.includes(why)) return [];
277
277
  return [{ kind: "note", text: `Agent: ${why}`, phase: "runtime" }];
278
278
  }
279
- function toSteps(groups) {
279
+ var APP_LOG_SOURCE = {
280
+ console: "console",
281
+ error: "uncaught",
282
+ network: "network",
283
+ system: "system"
284
+ };
285
+ function appLogNotes(lines) {
286
+ return lines.filter((l) => l.level === "error" || l.level === "warning").map((l) => ({
287
+ kind: "note",
288
+ text: `App ${APP_LOG_SOURCE[l.source] ?? l.source} ${l.level}: ${l.text}`,
289
+ phase: "runtime"
290
+ }));
291
+ }
292
+ function toSteps(groups, appLog) {
280
293
  const out = [];
281
294
  groups.forEach((group, index) => {
282
295
  const repeat = group.keyword !== "And" && group.keyword !== "But" && out.some((s) => s.keyword === group.keyword);
@@ -291,7 +304,8 @@ function toSteps(groups) {
291
304
  phase: "runtime"
292
305
  }
293
306
  ] : [],
294
- ...agentExplanation(c)
307
+ ...agentExplanation(c),
308
+ ...appLogNotes(appLog.filter((l) => l.step === c.index))
295
309
  ]);
296
310
  out.push({
297
311
  id: `step-${index}`,
@@ -304,6 +318,10 @@ function toSteps(groups) {
304
318
  });
305
319
  return out;
306
320
  }
321
+ function artifactName(artifact) {
322
+ const file = path2.basename(artifact.path ?? "");
323
+ return /-(?:expected|actual|diff|previous)\.png$/.test(file) ? file : artifact.kind;
324
+ }
307
325
  function toMedia(attempt, groups, artifactsRoot) {
308
326
  const attachments = [];
309
327
  const docs = [];
@@ -320,7 +338,7 @@ function toMedia(attempt, groups, artifactsRoot) {
320
338
  )
321
339
  );
322
340
  attachments.push({
323
- name: artifact.kind,
341
+ name: artifactName(artifact),
324
342
  mediaType: artifact.mediaType,
325
343
  path: path2.join(artifactsRoot, artifact.path),
326
344
  fileName: path2.basename(artifact.path),
@@ -344,7 +362,9 @@ function finalAttempt(result, report) {
344
362
  startedAt: member?.startedAt ?? last.startedAt,
345
363
  durationMs: member?.durationMs ?? last.durationMs,
346
364
  artifacts: last.artifacts,
347
- steps: member?.steps ?? []
365
+ steps: member?.steps ?? [],
366
+ ...member?.appLog ? { appLog: member.appLog } : {},
367
+ ...member?.skip ? { skip: member.skip } : {}
348
368
  },
349
369
  count: attempts.length
350
370
  };
@@ -362,7 +382,7 @@ function markersFor(result, attempt, report, markers, projectRoot) {
362
382
  (m) => m.file === file && m.line >= from && m.line < to && m.at >= start && m.at <= end
363
383
  ).sort((a, b) => a.at - b.at);
364
384
  }
365
- function toTestCase(result, report, artifactsRoot, markers, projectRoot) {
385
+ function toTestCase(result, report, artifactsRoot, markers, projectRoot, tracePage) {
366
386
  const scenario = result.titlePath.at(-1) ?? result.testId;
367
387
  const suitePath = result.titlePath.slice(0, -1);
368
388
  const { attempt, count } = finalAttempt(result, report);
@@ -377,6 +397,23 @@ function toTestCase(result, report, artifactsRoot, markers, projectRoot) {
377
397
  groups,
378
398
  artifactsRoot
379
399
  );
400
+ if (tracePage !== void 0) {
401
+ attachments.push({
402
+ name: "trace",
403
+ mediaType: "text/markdown",
404
+ path: path2.resolve(projectRoot, tracePage),
405
+ fileName: path2.basename(tracePage)
406
+ });
407
+ }
408
+ const appLog = attempt?.appLog ?? [];
409
+ docs.push(...appLogNotes(appLog.filter((l) => l.step === void 0)));
410
+ if (result.status !== "skipped" && attempt?.skip !== void 0) {
411
+ docs.push({
412
+ kind: "note",
413
+ text: `Skipped itself after a failure: ${attempt.skip.reason}`,
414
+ phase: "runtime"
415
+ });
416
+ }
380
417
  const error = attempt?.error;
381
418
  const details = error?.details;
382
419
  const stepEvents = groups.map((group, index) => {
@@ -396,7 +433,7 @@ function toTestCase(result, report, artifactsRoot, markers, projectRoot) {
396
433
  titlePath: [...result.titlePath],
397
434
  story: {
398
435
  scenario,
399
- steps: toSteps(groups),
436
+ steps: toSteps(groups, appLog),
400
437
  ...result.tags.length > 0 ? { tags: [...result.tags] } : {},
401
438
  ...suitePath.length > 0 ? { suitePath } : {},
402
439
  ...docs.length > 0 ? { docs } : {}
@@ -424,7 +461,14 @@ function toRawRun(args) {
424
461
  const filtered = args.lastRun !== void 0 || results.length < report.run.results.length;
425
462
  return {
426
463
  testCases: results.map(
427
- (r) => toTestCase(r, report, artifactsRoot, args.markers ?? [], projectRoot)
464
+ (r) => toTestCase(
465
+ r,
466
+ report,
467
+ artifactsRoot,
468
+ args.markers ?? [],
469
+ projectRoot,
470
+ args.traces?.get(r.id)
471
+ )
428
472
  ),
429
473
  startedAtMs: Date.parse(report.run.startedAt),
430
474
  finishedAtMs: Date.parse(report.run.finishedAt),
@@ -468,11 +512,13 @@ function storyReporter(options = {}) {
468
512
  await rm(markersDir, { recursive: true, force: true });
469
513
  return [{ label: "Stories", text: "skipped (explore run)" }];
470
514
  }
515
+ const traces = run.traces;
471
516
  const rawRun = toRawRun({
472
517
  report: run.report,
473
518
  projectRoot: run.projectRoot,
474
519
  artifactsRoot: run.artifactsRoot,
475
520
  markers: await readMarkers(markersDir),
521
+ ...traces ? { traces } : {},
476
522
  ...run.lastRun ? { lastRun: run.lastRun } : {}
477
523
  });
478
524
  await rm(markersDir, { recursive: true, force: true });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/phrase.ts","../src/story.ts"],"sourcesContent":["/**\n * executable-stories reporter for the `e2e` agentic test runner.\n *\n * The reporter reads the finished `report-1` document and writes the RawRun\n * JSON every other adapter writes. `executable-stories format` takes it from\n * there. Optional `story.given/when/then` markers in a test name the steps in\n * the test's own words; without them, step text is phrased from e2e's record.\n */\n\nimport { rmSync } from 'node:fs';\nimport { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { FinishedRun, Report, Reporter, ReporterSummary } from 'e2e';\nimport type {\n RawAttachment,\n RawRun,\n RawStatus,\n RawStepEvent,\n RawTestCase,\n} from 'executable-stories-core/types/raw';\nimport type {\n DocEntry,\n StepKeyword,\n StoryStep,\n} from 'executable-stories-core/types/story';\nimport { phraseStep } from './phrase';\nimport { MARKERS_DIR, type Marker } from './story';\n\nexport { story, type Marker, type MarkerKeyword } from './story';\nexport { phraseStep } from './phrase';\n\ntype ReportResult = Report['run']['results'][number];\ntype ReportAttempt = ReportResult['attempts'][number];\ntype ReportStep = ReportAttempt['steps'][number];\ntype ReportArtifact = ReportAttempt['artifacts'][number];\ntype ReportError = NonNullable<ReportAttempt['error']>;\n\nexport interface StoryReporterOptions {\n /** Where to write the RawRun JSON, relative to the project root. Default: `.executable-stories/raw-run.json`, where `executable-stories format` looks first. */\n rawRunPath?: string;\n}\n\n/**\n * Exhaustive over e2e's unions at compile time, forgiving at run time: a newer\n * e2e may report a kind or status this version has never seen, and its\n * contract says to ignore what you do not know rather than fail the reporter.\n */\nfunction unknownValue<T>(_value: never, fallback: T): T {\n return fallback;\n}\n\nfunction resultStatus(status: ReportResult['status']): RawStatus {\n switch (status) {\n case 'passed':\n case 'flaky': // the final attempt passed; `retry` records the failed ones\n return 'pass';\n case 'failed':\n return 'fail';\n case 'skipped':\n return 'skip';\n case 'timed-out':\n return 'timeout';\n case 'interrupted':\n return 'interrupted';\n default:\n return unknownValue(status, 'unknown');\n }\n}\n\nfunction stepStatus(status: ReportStep['status']): RawStatus {\n switch (status) {\n case 'passed':\n return 'pass';\n // Blocked and timed-out steps stopped the scenario, so they read as fail;\n // the error message keeps e2e's code.\n case 'failed':\n case 'blocked':\n case 'timed-out':\n return 'fail';\n // A cancelled step did not finish, so it reads as not run.\n case 'cancelled':\n return 'skip';\n default:\n return unknownValue(status, 'unknown');\n }\n}\n\n/** Arrange, act or assert, by what the step did. Repeats become `And` in {@link toSteps}. */\nfunction primaryKeyword(step: ReportStep): 'Given' | 'When' | 'Then' {\n switch (step.kind) {\n case 'app':\n case 'session':\n case 'resource':\n return 'Given';\n case 'locator':\n case 'screen':\n return 'When';\n case 'assertion':\n return 'Then';\n case 'agent':\n return step.api === 'agent.assert' ? 'Then' : 'When';\n default:\n return unknownValue(step.kind, 'When');\n }\n}\n\n/** Agent steps are prose already; the rest are phrased from the API they called on what. */\nfunction stepText(step: ReportStep): string {\n if (step.kind === 'agent' && step.label) return step.label;\n return phraseStep(step.api, step.label);\n}\n\nfunction errorMessage(error: ReportError): string {\n return `${error.code}: ${error.message}`;\n}\n\n/**\n * One rendered step: a marker with the e2e steps that ran under it, or a\n * single e2e step when the test has no markers before it.\n */\ninterface Group {\n keyword: Marker['keyword'];\n text: string;\n marker: boolean;\n children: ReportStep[];\n}\n\nfunction toGroups(\n steps: readonly ReportStep[],\n markers: readonly Marker[],\n projectRoot: string,\n): Group[] {\n const groups: Group[] = [];\n let pending = 0;\n let current: Group | undefined;\n // A marker goes before a step that started after it. Timestamps are whole\n // milliseconds, so on a tie the source position decides: a marker written\n // before the step in the same file, on an earlier line or earlier on the\n // same line, came first. Elsewhere, the marker wins.\n const before = (m: Marker, step: ReportStep | undefined): boolean => {\n if (step === undefined) return true;\n const at = Date.parse(step.startedAt);\n if (m.at !== at) return m.at < at;\n if (m.file !== path.resolve(projectRoot, step.source.file)) return true;\n return (\n m.line < step.source.line ||\n (m.line === step.source.line && (m.column ?? 0) < step.source.column)\n );\n };\n const takeMarkersBefore = (step: ReportStep | undefined): void => {\n while (pending < markers.length && before(markers[pending]!, step)) {\n const m = markers[pending++]!;\n current = {\n keyword: m.keyword,\n text: m.text,\n marker: true,\n children: [],\n };\n groups.push(current);\n }\n };\n for (const step of steps) {\n takeMarkersBefore(step);\n if (current) current.children.push(step);\n else\n groups.push({\n keyword: primaryKeyword(step),\n text: stepText(step),\n marker: false,\n children: [step],\n });\n }\n takeMarkersBefore(undefined);\n return groups;\n}\n\n/** A step that never reached a verdict (blocked, timed out, cancelled) asserted nothing we can see. */\nfunction isObservedClaim(step: ReportStep): boolean {\n return (\n primaryKeyword(step) === 'Then' &&\n (step.status === 'passed' || step.status === 'failed')\n );\n}\n\nfunction groupStatus(group: Group): RawStatus {\n const statuses = group.children.map((c) => stepStatus(c.status));\n for (const worst of ['fail', 'unknown', 'skip'] as const)\n if (statuses.includes(worst)) return worst;\n return 'pass';\n}\n\n/** The agent's explanation, unless the error the report shows already says it. */\nfunction agentExplanation(step: ReportStep): DocEntry[] {\n const why = step.explanation?.trim();\n if (!why || step.error?.message.includes(why)) return [];\n return [{ kind: 'note', text: `Agent: ${why}`, phase: 'runtime' }];\n}\n\nfunction toSteps(groups: readonly Group[]): StoryStep[] {\n const out: StoryStep[] = [];\n groups.forEach((group, index) => {\n const repeat =\n group.keyword !== 'And' &&\n group.keyword !== 'But' &&\n out.some((s) => s.keyword === group.keyword);\n const keyword: StepKeyword = repeat ? 'And' : group.keyword;\n const claims = group.children.filter(isObservedClaim).length;\n // e2e records locator and agent claims as steps. A plain value matcher\n // such as `expect(2).toBe(2)` records none, so a group without a recorded\n // claim leaves the count out.\n const assertions = claims > 0 ? { assertions: claims } : {};\n // A marker lists the e2e steps under it; any step adds the agent's own\n // account of what it did or why it judged a claim true.\n const docs: DocEntry[] = group.children.flatMap((c): DocEntry[] => [\n ...(group.marker\n ? [\n {\n kind: 'note' as const,\n text: stepText(c),\n phase: 'runtime' as const,\n },\n ]\n : []),\n ...agentExplanation(c),\n ]);\n out.push({\n id: `step-${index}`,\n keyword,\n text: group.text,\n durationMs: group.children.reduce((sum, c) => sum + c.durationMs, 0),\n ...assertions,\n ...(docs.length > 0 ? { docs } : {}),\n });\n });\n return out;\n}\n\n/**\n * Artifacts become attachments, the shape the Playwright adapter gives\n * screenshots, videos and traces. A step-produced artifact is pinned to its\n * step. A hosted video has a URL and no file, so it becomes a video doc.\n */\nfunction toMedia(\n attempt: {\n artifacts: readonly ReportArtifact[];\n steps: readonly ReportStep[];\n },\n groups: readonly Group[],\n artifactsRoot: string,\n): { attachments: RawAttachment[]; docs: DocEntry[] } {\n const attachments: RawAttachment[] = [];\n const docs: DocEntry[] = [];\n for (const artifact of attempt.artifacts) {\n if (artifact.path === undefined) {\n if (\n artifact.kind === 'video' &&\n artifact.url !== undefined &&\n /^https?:\\/\\//.test(artifact.url)\n ) {\n docs.push({ kind: 'video', path: artifact.url, phase: 'runtime' });\n }\n continue;\n }\n const stepIndex = groups.findIndex((group) =>\n group.children.some(\n (step) =>\n step.artifacts.includes(artifact.id) ||\n (artifact.producer?.kind === 'step' &&\n artifact.producer.stepId === step.id),\n ),\n );\n attachments.push({\n name: artifact.kind,\n mediaType: artifact.mediaType,\n path: path.join(artifactsRoot, artifact.path),\n fileName: path.basename(artifact.path),\n ...(artifact.size !== undefined ? { byteLength: artifact.size } : {}),\n ...(stepIndex >= 0 ? { stepIndex, stepId: `step-${stepIndex}` } : {}),\n });\n }\n return { attachments, docs };\n}\n\n/** The final attempt, wherever the report keeps it: a serial member's lives on its group. */\nfunction finalAttempt(\n result: ReportResult,\n report: Report,\n): {\n attempt?: Pick<\n ReportAttempt,\n 'error' | 'startedAt' | 'durationMs' | 'artifacts' | 'steps'\n >;\n count: number;\n} {\n if (result.serialGroupId === undefined) {\n return { attempt: result.attempts.at(-1), count: result.attempts.length };\n }\n const attempts =\n report.run.serialGroups.find((g) => g.id === result.serialGroupId)\n ?.attempts ?? [];\n const last = attempts.at(-1);\n const member = last?.members.find((m) => m.testId === result.testId);\n if (last === undefined) return { count: 0 };\n return {\n attempt: {\n error: member?.error ?? last.error,\n startedAt: member?.startedAt ?? last.startedAt,\n durationMs: member?.durationMs ?? last.durationMs,\n artifacts: last.artifacts,\n steps: member?.steps ?? [],\n },\n count: attempts.length,\n };\n}\n\n/**\n * The markers one attempt recorded: called from the test's own file, below its\n * declaration and above the next test's, while the attempt ran.\n */\nfunction markersFor(\n result: ReportResult,\n attempt: Pick<ReportAttempt, 'startedAt' | 'durationMs'> | undefined,\n report: Report,\n markers: readonly Marker[],\n projectRoot: string,\n): Marker[] {\n if (attempt === undefined || markers.length === 0) return [];\n const file = path.resolve(projectRoot, result.file);\n const from = result.source.line;\n const to = Math.min(\n ...report.run.results\n .filter((r) => r.file === result.file && r.source.line > from)\n .map((r) => r.source.line),\n );\n const start = Date.parse(attempt.startedAt);\n const end = start + attempt.durationMs + 1000; // slack for clock reads on either side\n return markers\n .filter(\n (m) =>\n m.file === file &&\n m.line >= from &&\n m.line < to &&\n m.at >= start &&\n m.at <= end,\n )\n .sort((a, b) => a.at - b.at);\n}\n\nfunction toTestCase(\n result: ReportResult,\n report: Report,\n artifactsRoot: string,\n markers: readonly Marker[],\n projectRoot: string,\n): RawTestCase {\n const scenario = result.titlePath.at(-1) ?? result.testId;\n const suitePath = result.titlePath.slice(0, -1);\n const { attempt, count } = finalAttempt(result, report);\n const steps = attempt?.steps ?? [];\n const groups = toGroups(\n steps,\n markersFor(result, attempt, report, markers, projectRoot),\n projectRoot,\n );\n const { attachments, docs } = toMedia(\n { artifacts: attempt?.artifacts ?? [], steps },\n groups,\n artifactsRoot,\n );\n const error = attempt?.error;\n const details = error?.details;\n const stepEvents: RawStepEvent[] = groups.map((group, index) => {\n const failed = group.children.find((c) => c.error !== undefined);\n return {\n index,\n stepId: `step-${index}`,\n title: group.text,\n status: groupStatus(group),\n durationMs: group.children.reduce((sum, c) => sum + c.durationMs, 0),\n ...(failed?.error ? { errorMessage: errorMessage(failed.error) } : {}),\n };\n });\n\n return {\n externalId: result.id,\n title: scenario,\n titlePath: [...result.titlePath],\n story: {\n scenario,\n steps: toSteps(groups),\n ...(result.tags.length > 0 ? { tags: [...result.tags] } : {}),\n ...(suitePath.length > 0 ? { suitePath } : {}),\n ...(docs.length > 0 ? { docs } : {}),\n },\n sourceFile: result.file,\n sourceLine: Math.max(1, result.source.line),\n status: resultStatus(result.status),\n durationMs: attempt?.durationMs ?? 0,\n ...(error\n ? {\n error: {\n message: errorMessage(error),\n ...(details?.expected !== undefined\n ? { expected: details.expected }\n : {}),\n ...(details?.observed !== undefined\n ? { actual: details.observed }\n : {}),\n },\n }\n : {}),\n ...(stepEvents.length > 0 ? { stepEvents } : {}),\n ...(attachments.length > 0 ? { attachments } : {}),\n retry: Math.max(0, count - 1),\n projectName: result.targetId,\n };\n}\n\n/**\n * Map an e2e `report-1` document to a RawRun. Pure: artifact paths resolve\n * against `artifactsRoot` and nothing is read from disk.\n */\nexport function toRawRun(args: {\n report: Report;\n projectRoot: string;\n artifactsRoot: string;\n /** Set when the run was `--last-failed`: it then covered only part of the suite. */\n lastRun?: Report;\n /** Step markers the run's tests recorded with `story.given/when/then`. */\n markers?: readonly Marker[];\n}): RawRun {\n const { report, projectRoot, artifactsRoot } = args;\n // Unselected results stay out of the docs.\n const results = report.run.results.filter((r) => r.selected !== false);\n const filtered =\n args.lastRun !== undefined || results.length < report.run.results.length;\n return {\n testCases: results.map((r) =>\n toTestCase(r, report, artifactsRoot, args.markers ?? [], projectRoot),\n ),\n startedAtMs: Date.parse(report.run.startedAt),\n finishedAtMs: Date.parse(report.run.finishedAt),\n projectRoot,\n // Only \"filtered\" is claimed: e2e can narrow a run in ways the report\n // does not record, so \"full\" (which retires scenarios) is never guessed.\n ...(filtered ? { runScope: 'filtered' as const } : {}),\n coveredSourceFiles: [...new Set(results.map((r) => r.file))].sort(),\n ...(report.run.vcs?.commit ? { gitSha: report.run.vcs.commit } : {}),\n };\n}\n\nasync function readMarkers(dir: string): Promise<Marker[]> {\n const files = await readdir(dir).catch(() => [] as string[]);\n const markers: Marker[] = [];\n for (const file of files.filter((f) => f.endsWith('.jsonl'))) {\n for (const line of (await readFile(path.join(dir, file), 'utf8')).split(\n '\\n',\n )) {\n if (!line) continue;\n try {\n markers.push(JSON.parse(line) as Marker);\n } catch {\n // a worker killed mid-write leaves a torn last line; skip it\n }\n }\n }\n return markers;\n}\n\n/** An e2e reporter that writes the run as RawRun JSON for the executable-stories CLI. */\nexport function storyReporter(options: StoryReporterOptions = {}): Reporter {\n return {\n name: 'executable-stories',\n onEvent(event) {\n // Start each run with an empty markers folder. The delete is small and\n // synchronous, which suits onEvent.\n if (event.type === 'run-started')\n rmSync(path.resolve(event.projectRoot, MARKERS_DIR), {\n recursive: true,\n force: true,\n });\n },\n async onRunFinished(run: FinishedRun): Promise<ReporterSummary> {\n const markersDir = path.resolve(run.projectRoot, MARKERS_DIR);\n // An explore run records findings, which the bug-bash skill turns into\n // scenarios. The last test run's raw-run.json stays as it is.\n if (run.report.run.explore !== undefined) {\n await rm(markersDir, { recursive: true, force: true });\n return [{ label: 'Stories', text: 'skipped (explore run)' }];\n }\n const rawRun = toRawRun({\n report: run.report,\n projectRoot: run.projectRoot,\n artifactsRoot: run.artifactsRoot,\n markers: await readMarkers(markersDir),\n ...(run.lastRun ? { lastRun: run.lastRun } : {}),\n });\n await rm(markersDir, { recursive: true, force: true });\n // A run that reached no test keeps the last test run's raw-run.json.\n if (rawRun.testCases.length === 0) {\n return [{ label: 'Stories', text: 'skipped (no tests ran)' }];\n }\n const target = path.resolve(\n run.projectRoot,\n options.rawRunPath ?? '.executable-stories/raw-run.json',\n );\n await mkdir(path.dirname(target), { recursive: true });\n await writeFile(\n target,\n JSON.stringify({ schemaVersion: 1, ...rawRun }, null, 2),\n 'utf8',\n );\n return [\n { label: 'Stories', text: path.relative(run.projectRoot, target) },\n ];\n },\n };\n}\n","/**\n * Plain-language step text for e2e's non-agent steps.\n *\n * e2e records a step as `api` + `label`, where the label is the locator's own\n * description (`getByRole(\"button\", name: \"Increment\")`). This module\n * rephrases the common shapes for a living-docs page. Other shapes keep the\n * raw `api label`.\n */\n\nconst ROLE_NOUNS: Record<string, string> = {\n textbox: 'field',\n searchbox: 'search field',\n combobox: 'dropdown',\n spinbutton: 'number field',\n img: 'image',\n};\n\n/** `getByKind(\"value\"[, name: \"name\"])` with plain string arguments only; scopes, filters, regexes and nth() stay raw. */\nconst SIMPLE_QUERY =\n /^getBy(\\w+)\\((\"(?:[^\"\\\\]|\\\\.)*\")(?:, name: (\"(?:[^\"\\\\]|\\\\.)*\"))?\\)$/;\n\nfunction unquote(json: string): string {\n try {\n return JSON.parse(json) as string;\n } catch {\n return json;\n }\n}\n\n/** A locator description as a noun phrase, or `undefined` when it is not a simple query. */\nexport function describeTarget(label: string): string | undefined {\n const match = SIMPLE_QUERY.exec(label);\n if (!match) return undefined;\n const [, kind, rawValue, rawName] = match;\n const value = unquote(rawValue!);\n const name = rawName === undefined ? undefined : unquote(rawName);\n switch (kind) {\n case 'Role': {\n const noun = ROLE_NOUNS[value] ?? value;\n return name === undefined ? `the ${noun}` : `the \"${name}\" ${noun}`;\n }\n case 'Text':\n return `the text \"${value}\"`;\n case 'Label':\n case 'Placeholder':\n return `the \"${value}\" field`;\n case 'AltText':\n return `the \"${value}\" image`;\n case 'TestId':\n case 'Title':\n return `the \"${value}\" element`;\n default:\n return undefined;\n }\n}\n\nconst ACTIONS: Record<string, string> = {\n click: 'click',\n tap: 'tap',\n dblclick: 'double-click',\n hover: 'hover over',\n check: 'check',\n uncheck: 'uncheck',\n fill: 'fill in',\n clear: 'clear',\n press: 'press a key on',\n pressSequentially: 'type into',\n selectOption: 'choose an option in',\n longPress: 'long-press',\n swipe: 'swipe',\n focus: 'focus',\n};\n\n/** `expect.toX` → what it claims about the target. */\nconst CLAIMS: Record<string, [positive: string, negative: string]> = {\n toBeVisible: ['is visible', 'is not visible'],\n toBeHidden: ['is hidden', 'is not hidden'],\n toBeEnabled: ['is enabled', 'is not enabled'],\n toBeDisabled: ['is disabled', 'is not disabled'],\n toBeChecked: ['is checked', 'is not checked'],\n toBeFocused: ['is focused', 'is not focused'],\n toBeSelected: ['is selected', 'is not selected'],\n toBeExpanded: ['is expanded', 'is not expanded'],\n toBeAttached: ['is on the page', 'is not on the page'],\n toHaveText: ['shows the expected text', 'does not show the given text'],\n toContainText: [\n 'contains the expected text',\n 'does not contain the given text',\n ],\n toHaveValue: ['has the expected value', 'does not have the given value'],\n toHaveCount: [\n 'appears the expected number of times',\n 'does not appear the given number of times',\n ],\n toHaveAccessibleName: [\n 'has the expected accessible name',\n 'does not have the given accessible name',\n ],\n toHaveAttribute: [\n 'has the expected attribute',\n 'does not have the given attribute',\n ],\n};\n\nconst APP: Record<string, string> = {\n 'app.back': 'go back',\n 'app.restart': 'restart the app',\n 'app.clearState': 'clear the app state',\n 'session.launch': 'launch the app session',\n 'session.close': 'close the app session',\n};\n\n/** Step text for one non-agent step. */\nexport function phraseStep(api: string, label: string): string {\n const raw = label ? `${api} ${label}` : api;\n\n if (api === 'app.open') return label ? `open ${label}` : 'open the app';\n if (api === 'app.screenshot')\n return label ? `take a screenshot: ${label}` : 'take a screenshot';\n if (api === 'session.save')\n return label ? `save the session as ${label}` : 'save the session';\n if (APP[api]) return APP[api];\n\n const action = /^locator\\.(\\w+)$/.exec(api)?.[1];\n if (action !== undefined) {\n if (action === 'waitFor') {\n // `<target> → <state>`\n const [target, state] = label.split(' → ');\n const noun = target === undefined ? undefined : describeTarget(target);\n return noun && state ? `wait until ${noun} is ${state}` : raw;\n }\n const verb = ACTIONS[action];\n const noun = describeTarget(label);\n return verb && noun ? `${verb} ${noun}` : raw;\n }\n\n const claim = /^expect\\.(not\\.)?(\\w+)$/.exec(api);\n if (claim) {\n const words = CLAIMS[claim[2]!];\n const noun = describeTarget(label);\n return words && noun ? `${noun} ${words[claim[1] ? 1 : 0]}` : raw;\n }\n\n return raw;\n}\n","/**\n * Step markers for e2e tests.\n *\n * A marker records where and when the test called it. The reporter groups\n * the e2e steps that ran after a marker under the marker's text:\n *\n * ```ts\n * test('incrementing shows the new count', async ({ app, screen }) => {\n * story.given('the counter page is open');\n * await app.open('/');\n * story.when('the user presses Increment twice');\n * await screen.getByRole('button', { name: 'Increment' }).click();\n * await screen.getByRole('button', { name: 'Increment' }).click();\n * story.then('the count reads 2');\n * await expect(screen.getByRole('status')).toHaveText('2');\n * });\n * ```\n */\n\nimport { appendFileSync, mkdirSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport type MarkerKeyword = 'Given' | 'When' | 'Then' | 'And' | 'But';\n\n/** One line of a markers file. */\nexport interface Marker {\n keyword: MarkerKeyword;\n text: string;\n /** Absolute path of the test file that called the marker. */\n file: string;\n line: number;\n /** Orders a marker before a step on its own line; absent reads as the line's start. */\n column?: number;\n /** Epoch milliseconds. */\n at: number;\n}\n\n/** Workers and the reporter both resolve this against the project root e2e runs from. */\nexport const MARKERS_DIR = '.executable-stories/e2e-markers';\n\nconst FRAME = /\\(?((?:file:\\/\\/)?[^\\s()]+?):(\\d+):(\\d+)\\)?$/;\n\n/** The first stack frame outside this module: the test line that called the marker. */\nfunction callSite():\n { file: string; line: number; column: number } | undefined {\n const frames = (new Error().stack ?? '').split('\\n').slice(1);\n let own: string | undefined;\n for (const frame of frames) {\n const match = FRAME.exec(frame.trim());\n if (!match) continue;\n const file = match[1]!.startsWith('file://')\n ? fileURLToPath(match[1]!)\n : match[1]!;\n own ??= file;\n if (file !== own)\n return { file, line: Number(match[2]), column: Number(match[3]) };\n }\n return undefined;\n}\n\nfunction mark(keyword: MarkerKeyword, text: string): void {\n const site = callSite();\n if (site === undefined) return;\n const marker: Marker = { keyword, text, ...site, at: Date.now() };\n const dir = path.resolve(MARKERS_DIR);\n mkdirSync(dir, { recursive: true });\n // One file per worker process; appends from one process stay in order.\n appendFileSync(\n path.join(dir, `${process.pid}.jsonl`),\n `${JSON.stringify(marker)}\\n`,\n );\n}\n\n/** Step markers. Call before the e2e steps the marker describes; they render under its text. */\nexport const story = {\n given: (text: string): void => mark('Given', text),\n when: (text: string): void => mark('When', text),\n then: (text: string): void => mark('Then', text),\n and: (text: string): void => mark('And', text),\n but: (text: string): void => mark('But', text),\n};\n"],"mappings":"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","names":["path","noun"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/phrase.ts","../src/story.ts"],"sourcesContent":["/**\n * executable-stories reporter for the `e2e` agentic test runner.\n *\n * The reporter reads the finished `report-1` document and writes the RawRun\n * JSON every other adapter writes. `executable-stories format` takes it from\n * there. Optional `story.given/when/then` markers in a test name the steps in\n * the test's own words; without them, step text is phrased from e2e's record.\n */\n\nimport { rmSync } from 'node:fs';\nimport { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { FinishedRun, Report, Reporter, ReporterSummary } from 'e2e';\nimport type {\n RawAttachment,\n RawRun,\n RawStatus,\n RawStepEvent,\n RawTestCase,\n} from 'executable-stories-core/types/raw';\nimport type {\n DocEntry,\n StepKeyword,\n StoryStep,\n} from 'executable-stories-core/types/story';\nimport { phraseStep } from './phrase';\nimport { MARKERS_DIR, type Marker } from './story';\n\nexport { story, type Marker, type MarkerKeyword } from './story';\nexport { phraseStep } from './phrase';\n\ntype ReportResult = Report['run']['results'][number];\ntype ReportAttempt = ReportResult['attempts'][number];\ntype ReportStep = ReportAttempt['steps'][number];\ntype ReportArtifact = ReportAttempt['artifacts'][number];\ntype ReportError = NonNullable<ReportAttempt['error']>;\n/**\n * One line of what the app did during an attempt (e2e 0.19+): console output,\n * an uncaught error, a failed request or a 4xx/5xx response, with the index of\n * the step that was running.\n */\ntype AppLogLine = NonNullable<ReportAttempt['appLog']>[number];\n\ntype FinalAttempt = Pick<\n ReportAttempt,\n | 'error'\n | 'startedAt'\n | 'durationMs'\n | 'artifacts'\n | 'steps'\n | 'appLog'\n | 'skip'\n>;\n\nexport interface StoryReporterOptions {\n /** Where to write the RawRun JSON, relative to the project root. Default: `.executable-stories/raw-run.json`, where `executable-stories format` looks first. */\n rawRunPath?: string;\n}\n\n/**\n * Exhaustive over e2e's unions at compile time, forgiving at run time: a newer\n * e2e may report a kind or status this version has never seen, and its\n * contract says to ignore what you do not know rather than fail the reporter.\n */\nfunction unknownValue<T>(_value: never, fallback: T): T {\n return fallback;\n}\n\nfunction resultStatus(status: ReportResult['status']): RawStatus {\n switch (status) {\n case 'passed':\n case 'flaky': // the final attempt passed; `retry` records the failed ones\n return 'pass';\n case 'failed':\n return 'fail';\n case 'skipped':\n return 'skip';\n case 'timed-out':\n return 'timeout';\n case 'interrupted':\n return 'interrupted';\n default:\n return unknownValue(status, 'unknown');\n }\n}\n\nfunction stepStatus(status: ReportStep['status']): RawStatus {\n switch (status) {\n case 'passed':\n return 'pass';\n // Blocked and timed-out steps stopped the scenario, so they read as fail;\n // the error message keeps e2e's code.\n case 'failed':\n case 'blocked':\n case 'timed-out':\n return 'fail';\n // A cancelled step did not finish, so it reads as not run.\n case 'cancelled':\n return 'skip';\n default:\n return unknownValue(status, 'unknown');\n }\n}\n\n/** Arrange, act or assert, by what the step did. Repeats become `And` in {@link toSteps}. */\nfunction primaryKeyword(step: ReportStep): 'Given' | 'When' | 'Then' {\n switch (step.kind) {\n case 'app':\n case 'session':\n case 'resource':\n return 'Given';\n case 'locator':\n case 'screen':\n return 'When';\n case 'assertion':\n return 'Then';\n case 'agent':\n return step.api === 'agent.assert' ? 'Then' : 'When';\n default:\n return unknownValue(step.kind, 'When');\n }\n}\n\n/** Agent steps are prose already; the rest are phrased from the API they called on what. */\nfunction stepText(step: ReportStep): string {\n if (step.kind === 'agent' && step.label) return step.label;\n return phraseStep(step.api, step.label);\n}\n\nfunction errorMessage(error: ReportError): string {\n return `${error.code}: ${error.message}`;\n}\n\n/**\n * One rendered step: a marker with the e2e steps that ran under it, or a\n * single e2e step when the test has no markers before it.\n */\ninterface Group {\n keyword: Marker['keyword'];\n text: string;\n marker: boolean;\n children: ReportStep[];\n}\n\nfunction toGroups(\n steps: readonly ReportStep[],\n markers: readonly Marker[],\n projectRoot: string,\n): Group[] {\n const groups: Group[] = [];\n let pending = 0;\n let current: Group | undefined;\n // A marker goes before a step that started after it. Timestamps are whole\n // milliseconds, so on a tie the source position decides: a marker written\n // before the step in the same file, on an earlier line or earlier on the\n // same line, came first. Elsewhere, the marker wins.\n const before = (m: Marker, step: ReportStep | undefined): boolean => {\n if (step === undefined) return true;\n const at = Date.parse(step.startedAt);\n if (m.at !== at) return m.at < at;\n if (m.file !== path.resolve(projectRoot, step.source.file)) return true;\n return (\n m.line < step.source.line ||\n (m.line === step.source.line && (m.column ?? 0) < step.source.column)\n );\n };\n const takeMarkersBefore = (step: ReportStep | undefined): void => {\n while (pending < markers.length && before(markers[pending]!, step)) {\n const m = markers[pending++]!;\n current = {\n keyword: m.keyword,\n text: m.text,\n marker: true,\n children: [],\n };\n groups.push(current);\n }\n };\n for (const step of steps) {\n takeMarkersBefore(step);\n if (current) current.children.push(step);\n else\n groups.push({\n keyword: primaryKeyword(step),\n text: stepText(step),\n marker: false,\n children: [step],\n });\n }\n takeMarkersBefore(undefined);\n return groups;\n}\n\n/** A step that never reached a verdict (blocked, timed out, cancelled) asserted nothing we can see. */\nfunction isObservedClaim(step: ReportStep): boolean {\n return (\n primaryKeyword(step) === 'Then' &&\n (step.status === 'passed' || step.status === 'failed')\n );\n}\n\nfunction groupStatus(group: Group): RawStatus {\n const statuses = group.children.map((c) => stepStatus(c.status));\n for (const worst of ['fail', 'unknown', 'skip'] as const)\n if (statuses.includes(worst)) return worst;\n return 'pass';\n}\n\n/** The agent's explanation, unless the error the report shows already says it. */\nfunction agentExplanation(step: ReportStep): DocEntry[] {\n const why = step.explanation?.trim();\n if (!why || step.error?.message.includes(why)) return [];\n return [{ kind: 'note', text: `Agent: ${why}`, phase: 'runtime' }];\n}\n\nconst APP_LOG_SOURCE: Readonly<Record<string, string>> = {\n console: 'console',\n error: 'uncaught',\n network: 'network',\n system: 'system',\n};\n\n/** Warnings and errors the app logged while a step ran; info lines are noise in docs. */\nfunction appLogNotes(lines: readonly AppLogLine[]): DocEntry[] {\n return lines\n .filter((l) => l.level === 'error' || l.level === 'warning')\n .map((l) => ({\n kind: 'note' as const,\n text: `App ${APP_LOG_SOURCE[l.source] ?? l.source} ${l.level}: ${l.text}`,\n phase: 'runtime' as const,\n }));\n}\n\nfunction toSteps(\n groups: readonly Group[],\n appLog: readonly AppLogLine[],\n): StoryStep[] {\n const out: StoryStep[] = [];\n groups.forEach((group, index) => {\n const repeat =\n group.keyword !== 'And' &&\n group.keyword !== 'But' &&\n out.some((s) => s.keyword === group.keyword);\n const keyword: StepKeyword = repeat ? 'And' : group.keyword;\n const claims = group.children.filter(isObservedClaim).length;\n // e2e records locator and agent claims as steps. A plain value matcher\n // such as `expect(2).toBe(2)` records none, so a group without a recorded\n // claim leaves the count out.\n const assertions = claims > 0 ? { assertions: claims } : {};\n // A marker lists the e2e steps under it; any step adds the agent's own\n // account of what it did or why it judged a claim true.\n const docs: DocEntry[] = group.children.flatMap((c): DocEntry[] => [\n ...(group.marker\n ? [\n {\n kind: 'note' as const,\n text: stepText(c),\n phase: 'runtime' as const,\n },\n ]\n : []),\n ...agentExplanation(c),\n ...appLogNotes(appLog.filter((l) => l.step === c.index)),\n ]);\n out.push({\n id: `step-${index}`,\n keyword,\n text: group.text,\n durationMs: group.children.reduce((sum, c) => sum + c.durationMs, 0),\n ...assertions,\n ...(docs.length > 0 ? { docs } : {}),\n });\n });\n return out;\n}\n\n/**\n * A failed `toHaveScreenshot` attaches `<stem>-expected.png`, `-actual.png`\n * and `-diff.png` to its step, all of kind `screenshot`. They keep their file\n * name, as Playwright names the same trio, so the report pairs them.\n */\nfunction artifactName(artifact: ReportArtifact): string {\n const file = path.basename(artifact.path ?? '');\n return /-(?:expected|actual|diff|previous)\\.png$/.test(file)\n ? file\n : artifact.kind;\n}\n\n/**\n * Artifacts become attachments, the shape the Playwright adapter gives\n * screenshots, videos and traces. A step-produced artifact is pinned to its\n * step. A hosted video has a URL and no file, so it becomes a video doc.\n */\nfunction toMedia(\n attempt: {\n artifacts: readonly ReportArtifact[];\n steps: readonly ReportStep[];\n },\n groups: readonly Group[],\n artifactsRoot: string,\n): { attachments: RawAttachment[]; docs: DocEntry[] } {\n const attachments: RawAttachment[] = [];\n const docs: DocEntry[] = [];\n for (const artifact of attempt.artifacts) {\n if (artifact.path === undefined) {\n if (\n artifact.kind === 'video' &&\n artifact.url !== undefined &&\n /^https?:\\/\\//.test(artifact.url)\n ) {\n docs.push({ kind: 'video', path: artifact.url, phase: 'runtime' });\n }\n continue;\n }\n const stepIndex = groups.findIndex((group) =>\n group.children.some(\n (step) =>\n step.artifacts.includes(artifact.id) ||\n (artifact.producer?.kind === 'step' &&\n artifact.producer.stepId === step.id),\n ),\n );\n attachments.push({\n name: artifactName(artifact),\n mediaType: artifact.mediaType,\n path: path.join(artifactsRoot, artifact.path),\n fileName: path.basename(artifact.path),\n ...(artifact.size !== undefined ? { byteLength: artifact.size } : {}),\n ...(stepIndex >= 0 ? { stepIndex, stepId: `step-${stepIndex}` } : {}),\n });\n }\n return { attachments, docs };\n}\n\n/** The final attempt, wherever the report keeps it: a serial member's lives on its group. */\nfunction finalAttempt(\n result: ReportResult,\n report: Report,\n): {\n attempt?: FinalAttempt;\n count: number;\n} {\n if (result.serialGroupId === undefined) {\n return { attempt: result.attempts.at(-1), count: result.attempts.length };\n }\n const attempts =\n report.run.serialGroups.find((g) => g.id === result.serialGroupId)\n ?.attempts ?? [];\n const last = attempts.at(-1);\n const member = last?.members.find((m) => m.testId === result.testId);\n if (last === undefined) return { count: 0 };\n return {\n attempt: {\n error: member?.error ?? last.error,\n startedAt: member?.startedAt ?? last.startedAt,\n durationMs: member?.durationMs ?? last.durationMs,\n artifacts: last.artifacts,\n steps: member?.steps ?? [],\n ...(member?.appLog ? { appLog: member.appLog } : {}),\n ...(member?.skip ? { skip: member.skip } : {}),\n },\n count: attempts.length,\n };\n}\n\n/**\n * The markers one attempt recorded: called from the test's own file, below its\n * declaration and above the next test's, while the attempt ran.\n */\nfunction markersFor(\n result: ReportResult,\n attempt: Pick<ReportAttempt, 'startedAt' | 'durationMs'> | undefined,\n report: Report,\n markers: readonly Marker[],\n projectRoot: string,\n): Marker[] {\n if (attempt === undefined || markers.length === 0) return [];\n const file = path.resolve(projectRoot, result.file);\n const from = result.source.line;\n const to = Math.min(\n ...report.run.results\n .filter((r) => r.file === result.file && r.source.line > from)\n .map((r) => r.source.line),\n );\n const start = Date.parse(attempt.startedAt);\n const end = start + attempt.durationMs + 1000; // slack for clock reads on either side\n return markers\n .filter(\n (m) =>\n m.file === file &&\n m.line >= from &&\n m.line < to &&\n m.at >= start &&\n m.at <= end,\n )\n .sort((a, b) => a.at - b.at);\n}\n\nfunction toTestCase(\n result: ReportResult,\n report: Report,\n artifactsRoot: string,\n markers: readonly Marker[],\n projectRoot: string,\n tracePage: string | undefined,\n): RawTestCase {\n const scenario = result.titlePath.at(-1) ?? result.testId;\n const suitePath = result.titlePath.slice(0, -1);\n const { attempt, count } = finalAttempt(result, report);\n const steps = attempt?.steps ?? [];\n const groups = toGroups(\n steps,\n markersFor(result, attempt, report, markers, projectRoot),\n projectRoot,\n );\n const { attachments, docs } = toMedia(\n { artifacts: attempt?.artifacts ?? [], steps },\n groups,\n artifactsRoot,\n );\n // e2e 0.19 writes a markdown trace page per test that kept one (failed\n // tests by default): what each step did, read and logged.\n if (tracePage !== undefined) {\n attachments.push({\n name: 'trace',\n mediaType: 'text/markdown',\n path: path.resolve(projectRoot, tracePage),\n fileName: path.basename(tracePage),\n });\n }\n const appLog = attempt?.appLog ?? [];\n // Lines logged before the first step have no step to sit under.\n docs.push(...appLogNotes(appLog.filter((l) => l.step === undefined)));\n if (result.status !== 'skipped' && attempt?.skip !== undefined) {\n docs.push({\n kind: 'note',\n text: `Skipped itself after a failure: ${attempt.skip.reason}`,\n phase: 'runtime',\n });\n }\n const error = attempt?.error;\n const details = error?.details;\n const stepEvents: RawStepEvent[] = groups.map((group, index) => {\n const failed = group.children.find((c) => c.error !== undefined);\n return {\n index,\n stepId: `step-${index}`,\n title: group.text,\n status: groupStatus(group),\n durationMs: group.children.reduce((sum, c) => sum + c.durationMs, 0),\n ...(failed?.error ? { errorMessage: errorMessage(failed.error) } : {}),\n };\n });\n\n return {\n externalId: result.id,\n title: scenario,\n titlePath: [...result.titlePath],\n story: {\n scenario,\n steps: toSteps(groups, appLog),\n ...(result.tags.length > 0 ? { tags: [...result.tags] } : {}),\n ...(suitePath.length > 0 ? { suitePath } : {}),\n ...(docs.length > 0 ? { docs } : {}),\n },\n sourceFile: result.file,\n sourceLine: Math.max(1, result.source.line),\n status: resultStatus(result.status),\n durationMs: attempt?.durationMs ?? 0,\n ...(error\n ? {\n error: {\n message: errorMessage(error),\n ...(details?.expected !== undefined\n ? { expected: details.expected }\n : {}),\n ...(details?.observed !== undefined\n ? { actual: details.observed }\n : {}),\n },\n }\n : {}),\n ...(stepEvents.length > 0 ? { stepEvents } : {}),\n ...(attachments.length > 0 ? { attachments } : {}),\n retry: Math.max(0, count - 1),\n projectName: result.targetId,\n };\n}\n\n/**\n * Map an e2e `report-1` document to a RawRun. Pure: artifact paths resolve\n * against `artifactsRoot` and nothing is read from disk.\n */\nexport function toRawRun(args: {\n report: Report;\n projectRoot: string;\n artifactsRoot: string;\n /** Set when the run was `--last-failed`: it then covered only part of the suite. */\n lastRun?: Report;\n /** Step markers the run's tests recorded with `story.given/when/then`. */\n markers?: readonly Marker[];\n /** e2e 0.19+: each kept `trace.md` page by result id, as a path from the project root. */\n traces?: ReadonlyMap<string, string>;\n}): RawRun {\n const { report, projectRoot, artifactsRoot } = args;\n // Unselected results stay out of the docs.\n const results = report.run.results.filter((r) => r.selected !== false);\n const filtered =\n args.lastRun !== undefined || results.length < report.run.results.length;\n return {\n testCases: results.map((r) =>\n toTestCase(\n r,\n report,\n artifactsRoot,\n args.markers ?? [],\n projectRoot,\n args.traces?.get(r.id),\n ),\n ),\n startedAtMs: Date.parse(report.run.startedAt),\n finishedAtMs: Date.parse(report.run.finishedAt),\n projectRoot,\n // Only \"filtered\" is claimed: e2e can narrow a run in ways the report\n // does not record, so \"full\" (which retires scenarios) is never guessed.\n ...(filtered ? { runScope: 'filtered' as const } : {}),\n coveredSourceFiles: [...new Set(results.map((r) => r.file))].sort(),\n ...(report.run.vcs?.commit ? { gitSha: report.run.vcs.commit } : {}),\n };\n}\n\nasync function readMarkers(dir: string): Promise<Marker[]> {\n const files = await readdir(dir).catch(() => [] as string[]);\n const markers: Marker[] = [];\n for (const file of files.filter((f) => f.endsWith('.jsonl'))) {\n for (const line of (await readFile(path.join(dir, file), 'utf8')).split(\n '\\n',\n )) {\n if (!line) continue;\n try {\n markers.push(JSON.parse(line) as Marker);\n } catch {\n // a worker killed mid-write leaves a torn last line; skip it\n }\n }\n }\n return markers;\n}\n\n/** An e2e reporter that writes the run as RawRun JSON for the executable-stories CLI. */\nexport function storyReporter(options: StoryReporterOptions = {}): Reporter {\n return {\n name: 'executable-stories',\n onEvent(event) {\n // Start each run with an empty markers folder. The delete is small and\n // synchronous, which suits onEvent.\n if (event.type === 'run-started')\n rmSync(path.resolve(event.projectRoot, MARKERS_DIR), {\n recursive: true,\n force: true,\n });\n },\n async onRunFinished(run: FinishedRun): Promise<ReporterSummary> {\n const markersDir = path.resolve(run.projectRoot, MARKERS_DIR);\n // An explore run records findings, which the bug-bash skill turns into\n // scenarios. The last test run's raw-run.json stays as it is.\n if (run.report.run.explore !== undefined) {\n await rm(markersDir, { recursive: true, force: true });\n return [{ label: 'Stories', text: 'skipped (explore run)' }];\n }\n // Added in e2e 0.19; undefined on an older runner.\n const traces = run.traces as FinishedRun['traces'] | undefined;\n const rawRun = toRawRun({\n report: run.report,\n projectRoot: run.projectRoot,\n artifactsRoot: run.artifactsRoot,\n markers: await readMarkers(markersDir),\n ...(traces ? { traces } : {}),\n ...(run.lastRun ? { lastRun: run.lastRun } : {}),\n });\n await rm(markersDir, { recursive: true, force: true });\n // A run that reached no test keeps the last test run's raw-run.json.\n if (rawRun.testCases.length === 0) {\n return [{ label: 'Stories', text: 'skipped (no tests ran)' }];\n }\n const target = path.resolve(\n run.projectRoot,\n options.rawRunPath ?? '.executable-stories/raw-run.json',\n );\n await mkdir(path.dirname(target), { recursive: true });\n await writeFile(\n target,\n JSON.stringify({ schemaVersion: 1, ...rawRun }, null, 2),\n 'utf8',\n );\n return [\n { label: 'Stories', text: path.relative(run.projectRoot, target) },\n ];\n },\n };\n}\n","/**\n * Plain-language step text for e2e's non-agent steps.\n *\n * e2e records a step as `api` + `label`, where the label is the locator's own\n * description (`getByRole(\"button\", name: \"Increment\")`). This module\n * rephrases the common shapes for a living-docs page. Other shapes keep the\n * raw `api label`.\n */\n\nconst ROLE_NOUNS: Record<string, string> = {\n textbox: 'field',\n searchbox: 'search field',\n combobox: 'dropdown',\n spinbutton: 'number field',\n img: 'image',\n};\n\n/** `getByKind(\"value\"[, name: \"name\"])` with plain string arguments only; scopes, filters, regexes and nth() stay raw. */\nconst SIMPLE_QUERY =\n /^getBy(\\w+)\\((\"(?:[^\"\\\\]|\\\\.)*\")(?:, name: (\"(?:[^\"\\\\]|\\\\.)*\"))?\\)$/;\n\nfunction unquote(json: string): string {\n try {\n return JSON.parse(json) as string;\n } catch {\n return json;\n }\n}\n\n/** A locator description as a noun phrase, or `undefined` when it is not a simple query. */\nexport function describeTarget(label: string): string | undefined {\n const match = SIMPLE_QUERY.exec(label);\n if (!match) return undefined;\n const [, kind, rawValue, rawName] = match;\n const value = unquote(rawValue!);\n const name = rawName === undefined ? undefined : unquote(rawName);\n switch (kind) {\n case 'Role': {\n const noun = ROLE_NOUNS[value] ?? value;\n return name === undefined ? `the ${noun}` : `the \"${name}\" ${noun}`;\n }\n case 'Text':\n return `the text \"${value}\"`;\n case 'Label':\n case 'Placeholder':\n return `the \"${value}\" field`;\n case 'AltText':\n return `the \"${value}\" image`;\n case 'TestId':\n case 'Title':\n return `the \"${value}\" element`;\n default:\n return undefined;\n }\n}\n\nconst ACTIONS: Record<string, string> = {\n click: 'click',\n tap: 'tap',\n dblclick: 'double-click',\n hover: 'hover over',\n check: 'check',\n uncheck: 'uncheck',\n fill: 'fill in',\n clear: 'clear',\n press: 'press a key on',\n pressSequentially: 'type into',\n selectOption: 'choose an option in',\n longPress: 'long-press',\n swipe: 'swipe',\n focus: 'focus',\n};\n\n/** `expect.toX` → what it claims about the target. */\nconst CLAIMS: Record<string, [positive: string, negative: string]> = {\n toBeVisible: ['is visible', 'is not visible'],\n toBeHidden: ['is hidden', 'is not hidden'],\n toBeEnabled: ['is enabled', 'is not enabled'],\n toBeDisabled: ['is disabled', 'is not disabled'],\n toBeChecked: ['is checked', 'is not checked'],\n toBeFocused: ['is focused', 'is not focused'],\n toBeSelected: ['is selected', 'is not selected'],\n toBeExpanded: ['is expanded', 'is not expanded'],\n toBeAttached: ['is on the page', 'is not on the page'],\n toHaveText: ['shows the expected text', 'does not show the given text'],\n toContainText: [\n 'contains the expected text',\n 'does not contain the given text',\n ],\n toHaveValue: ['has the expected value', 'does not have the given value'],\n toHaveCount: [\n 'appears the expected number of times',\n 'does not appear the given number of times',\n ],\n toHaveAccessibleName: [\n 'has the expected accessible name',\n 'does not have the given accessible name',\n ],\n toHaveAttribute: [\n 'has the expected attribute',\n 'does not have the given attribute',\n ],\n};\n\nconst APP: Record<string, string> = {\n 'app.back': 'go back',\n 'app.restart': 'restart the app',\n 'app.clearState': 'clear the app state',\n 'session.launch': 'launch the app session',\n 'session.close': 'close the app session',\n};\n\n/** Step text for one non-agent step. */\nexport function phraseStep(api: string, label: string): string {\n const raw = label ? `${api} ${label}` : api;\n\n if (api === 'app.open') return label ? `open ${label}` : 'open the app';\n if (api === 'app.screenshot')\n return label ? `take a screenshot: ${label}` : 'take a screenshot';\n if (api === 'session.save')\n return label ? `save the session as ${label}` : 'save the session';\n if (APP[api]) return APP[api];\n\n const action = /^locator\\.(\\w+)$/.exec(api)?.[1];\n if (action !== undefined) {\n if (action === 'waitFor') {\n // `<target> → <state>`\n const [target, state] = label.split(' → ');\n const noun = target === undefined ? undefined : describeTarget(target);\n return noun && state ? `wait until ${noun} is ${state}` : raw;\n }\n const verb = ACTIONS[action];\n const noun = describeTarget(label);\n return verb && noun ? `${verb} ${noun}` : raw;\n }\n\n const claim = /^expect\\.(not\\.)?(\\w+)$/.exec(api);\n if (claim) {\n const words = CLAIMS[claim[2]!];\n const noun = describeTarget(label);\n return words && noun ? `${noun} ${words[claim[1] ? 1 : 0]}` : raw;\n }\n\n return raw;\n}\n","/**\n * Step markers for e2e tests.\n *\n * A marker records where and when the test called it. The reporter groups\n * the e2e steps that ran after a marker under the marker's text:\n *\n * ```ts\n * test('incrementing shows the new count', async ({ app, screen }) => {\n * story.given('the counter page is open');\n * await app.open('/');\n * story.when('the user presses Increment twice');\n * await screen.getByRole('button', { name: 'Increment' }).click();\n * await screen.getByRole('button', { name: 'Increment' }).click();\n * story.then('the count reads 2');\n * await expect(screen.getByRole('status')).toHaveText('2');\n * });\n * ```\n */\n\nimport { appendFileSync, mkdirSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport type MarkerKeyword = 'Given' | 'When' | 'Then' | 'And' | 'But';\n\n/** One line of a markers file. */\nexport interface Marker {\n keyword: MarkerKeyword;\n text: string;\n /** Absolute path of the test file that called the marker. */\n file: string;\n line: number;\n /** Orders a marker before a step on its own line; absent reads as the line's start. */\n column?: number;\n /** Epoch milliseconds. */\n at: number;\n}\n\n/** Workers and the reporter both resolve this against the project root e2e runs from. */\nexport const MARKERS_DIR = '.executable-stories/e2e-markers';\n\nconst FRAME = /\\(?((?:file:\\/\\/)?[^\\s()]+?):(\\d+):(\\d+)\\)?$/;\n\n/** The first stack frame outside this module: the test line that called the marker. */\nfunction callSite():\n { file: string; line: number; column: number } | undefined {\n const frames = (new Error().stack ?? '').split('\\n').slice(1);\n let own: string | undefined;\n for (const frame of frames) {\n const match = FRAME.exec(frame.trim());\n if (!match) continue;\n const file = match[1]!.startsWith('file://')\n ? fileURLToPath(match[1]!)\n : match[1]!;\n own ??= file;\n if (file !== own)\n return { file, line: Number(match[2]), column: Number(match[3]) };\n }\n return undefined;\n}\n\nfunction mark(keyword: MarkerKeyword, text: string): void {\n const site = callSite();\n if (site === undefined) return;\n const marker: Marker = { keyword, text, ...site, at: Date.now() };\n const dir = path.resolve(MARKERS_DIR);\n mkdirSync(dir, { recursive: true });\n // One file per worker process; appends from one process stay in order.\n appendFileSync(\n path.join(dir, `${process.pid}.jsonl`),\n `${JSON.stringify(marker)}\\n`,\n );\n}\n\n/** Step markers. Call before the e2e steps the marker describes; they render under its text. */\nexport const story = {\n given: (text: string): void => mark('Given', text),\n when: (text: string): void => mark('When', text),\n then: (text: string): void => mark('Then', text),\n and: (text: string): void => mark('And', text),\n but: (text: string): void => mark('But', text),\n};\n"],"mappings":"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","names":["path","noun"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-e2e",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "executable-stories reporter for the e2e agentic test runner: turns its report-1 document into story docs",
6
6
  "type": "module",
@@ -27,13 +27,13 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^26.6.1",
30
- "e2e": "0.15.1",
30
+ "e2e": "0.20.0",
31
31
  "tsup": "^8.5.1",
32
32
  "typescript": "~6.0.3",
33
33
  "vitest": "^5.0.1",
34
34
  "eslint-config-executable-stories": "0.2.0",
35
- "executable-stories-formatters": "1.23.0",
36
- "executable-stories-vitest": "8.10.1"
35
+ "executable-stories-formatters": "1.24.1",
36
+ "executable-stories-vitest": "8.10.3"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=22.12.0"