playwright-checkpoint 0.3.0 → 0.4.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.
Files changed (42) hide show
  1. package/README.md +57 -0
  2. package/dist/{chunk-KG37WSYS.js → chunk-M3BRR3LT.js} +9 -3
  3. package/dist/{chunk-KG37WSYS.js.map → chunk-M3BRR3LT.js.map} +1 -1
  4. package/dist/{chunk-X5IPL32H.js → chunk-WXZOP7XI.js} +153 -35
  5. package/dist/chunk-WXZOP7XI.js.map +1 -0
  6. package/dist/{chunk-K5DX32TO.js → chunk-YUFXGGZM.js} +2 -2
  7. package/dist/cli/bin.cjs +2501 -2386
  8. package/dist/cli/bin.cjs.map +1 -1
  9. package/dist/cli/bin.js +3 -2
  10. package/dist/cli/bin.js.map +1 -1
  11. package/dist/cli/index.cjs +1405 -68
  12. package/dist/cli/index.cjs.map +1 -1
  13. package/dist/cli/index.d.cts +2 -2
  14. package/dist/cli/index.d.ts +2 -2
  15. package/dist/cli/index.js +3 -2
  16. package/dist/{core-CD4jHGgI.d.cts → core-6gyzs35M.d.ts} +2 -1
  17. package/dist/{core-CZvnc0rE.d.ts → core-Dd3WLuTs.d.cts} +2 -1
  18. package/dist/core.cjs +8 -2
  19. package/dist/core.cjs.map +1 -1
  20. package/dist/core.d.cts +2 -2
  21. package/dist/core.d.ts +2 -2
  22. package/dist/core.js +1 -1
  23. package/dist/{index-BjYQX_hK.d.ts → index-CvcgBzvl.d.ts} +1 -1
  24. package/dist/{index-Cabk31qi.d.cts → index-OQx9qcVO.d.cts} +1 -1
  25. package/dist/index.cjs +212 -38
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +4 -4
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +69 -15
  30. package/dist/index.js.map +1 -1
  31. package/dist/mcp/index.cjs +148 -34
  32. package/dist/mcp/index.cjs.map +1 -1
  33. package/dist/mcp/index.js +4 -4
  34. package/dist/teardown.cjs +1409 -72
  35. package/dist/teardown.cjs.map +1 -1
  36. package/dist/teardown.js +3 -2
  37. package/dist/teardown.js.map +1 -1
  38. package/dist/{types-G7w4n8kR.d.cts → types-wX4eB9mb.d.cts} +16 -1
  39. package/dist/{types-G7w4n8kR.d.ts → types-wX4eB9mb.d.ts} +16 -1
  40. package/package.json +2 -1
  41. package/dist/chunk-X5IPL32H.js.map +0 -1
  42. /package/dist/{chunk-K5DX32TO.js.map → chunk-YUFXGGZM.js.map} +0 -0
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
- import { T as TestCheckpointConfig, C as CheckpointConfig, e as CheckpointManifest, f as CheckpointOptions, g as CheckpointRecord, c as CheckpointCollector, a as ReportGenerator, R as RunRecord, B as BoundingBox } from './types-G7w4n8kR.cjs';
2
- export { A as AriaSnapshotCollectorData, i as AxeCollectorData, j as CollectorArtifact, d as CollectorConfig, k as CollectorContext, l as CollectorOptions, m as CollectorResult, n as ConsoleErrorRecord, D as DomStatsCollectorData, F as FailedRequestRecord, o as FormFieldState, p as FormFieldValue, q as FormsCollectorData, H as HtmlCollectorData, N as NetworkTimingBreakdown, r as NetworkTimingCollectorData, s as NetworkTimingRecord, P as PageMetadata, b as ReportGenerationResults, t as ReportGeneratorContext, u as ReportGeneratorResult, v as ReporterConfig, h as ResolvedCollectorConfig, S as ScreenshotCollectorData, w as StorageCollectorData, x as StorageCookieState, y as StorageEntryState, W as WebVitalMetric, z as WebVitalRating, E as WebVitalsSnapshot } from './types-G7w4n8kR.cjs';
3
- export { C as CaptureCheckpointOptions, a as CheckpointSession, b as CheckpointSessionMetadata, c as CheckpointSessionOptions, R as RunCollectorPipelineArgs, d as captureCheckpoint, e as checkpointSlug, f as collectPageTitle, g as createCheckpointSession, h as getBuiltinCollectors, r as registerBuiltinCollector, i as registerBuiltinCollectors, j as resolveCollectors, k as runCollectorPipeline, l as runCollectorSetup, m as runCollectorTeardown, s as sanitizeSegment, n as settlePage, w as warn } from './core-CD4jHGgI.cjs';
1
+ import { T as TestCheckpointConfig, C as CheckpointConfig, e as CheckpointManifest, f as CheckpointOptions, g as CheckpointRecord, c as CheckpointCollector, a as ReportGenerator, R as RunRecord, B as BoundingBox } from './types-wX4eB9mb.cjs';
2
+ export { A as AriaSnapshotCollectorData, i as ArticleDefinition, j as ArticleMetadata, k as AxeCollectorData, l as CollectorArtifact, d as CollectorConfig, m as CollectorContext, n as CollectorOptions, o as CollectorResult, p as ConsoleErrorRecord, D as DomStatsCollectorData, F as FailedRequestRecord, q as FormFieldState, r as FormFieldValue, s as FormsCollectorData, H as HtmlCollectorData, N as NetworkTimingBreakdown, t as NetworkTimingCollectorData, u as NetworkTimingRecord, P as PageMetadata, b as ReportGenerationResults, v as ReportGeneratorContext, w as ReportGeneratorResult, x as ReporterConfig, h as ResolvedCollectorConfig, S as ScreenshotCollectorData, y as StorageCollectorData, z as StorageCookieState, E as StorageEntryState, W as WebVitalMetric, G as WebVitalRating, I as WebVitalsSnapshot } from './types-wX4eB9mb.cjs';
3
+ export { C as CaptureCheckpointOptions, a as CheckpointSession, b as CheckpointSessionMetadata, c as CheckpointSessionOptions, R as RunCollectorPipelineArgs, d as captureCheckpoint, e as checkpointSlug, f as collectPageTitle, g as createCheckpointSession, h as getBuiltinCollectors, r as registerBuiltinCollector, i as registerBuiltinCollectors, j as resolveCollectors, k as runCollectorPipeline, l as runCollectorSetup, m as runCollectorTeardown, s as sanitizeSegment, n as settlePage, w as warn } from './core-Dd3WLuTs.cjs';
4
4
  import * as PlaywrightModule from '@playwright/test';
5
5
  import { TestInfo, Page, TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '@playwright/test';
6
- export { d as dedupeRuns, l as loadRuns, a as registerBuiltinReporter, r as runReporters } from './index-Cabk31qi.cjs';
6
+ export { d as dedupeRuns, l as loadRuns, a as registerBuiltinReporter, r as runReporters } from './index-OQx9qcVO.cjs';
7
7
 
8
8
  type DeviceSurface = 'desktop' | 'mobile';
9
9
  type DeviceProfile = {
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { T as TestCheckpointConfig, C as CheckpointConfig, e as CheckpointManifest, f as CheckpointOptions, g as CheckpointRecord, c as CheckpointCollector, a as ReportGenerator, R as RunRecord, B as BoundingBox } from './types-G7w4n8kR.js';
2
- export { A as AriaSnapshotCollectorData, i as AxeCollectorData, j as CollectorArtifact, d as CollectorConfig, k as CollectorContext, l as CollectorOptions, m as CollectorResult, n as ConsoleErrorRecord, D as DomStatsCollectorData, F as FailedRequestRecord, o as FormFieldState, p as FormFieldValue, q as FormsCollectorData, H as HtmlCollectorData, N as NetworkTimingBreakdown, r as NetworkTimingCollectorData, s as NetworkTimingRecord, P as PageMetadata, b as ReportGenerationResults, t as ReportGeneratorContext, u as ReportGeneratorResult, v as ReporterConfig, h as ResolvedCollectorConfig, S as ScreenshotCollectorData, w as StorageCollectorData, x as StorageCookieState, y as StorageEntryState, W as WebVitalMetric, z as WebVitalRating, E as WebVitalsSnapshot } from './types-G7w4n8kR.js';
3
- export { C as CaptureCheckpointOptions, a as CheckpointSession, b as CheckpointSessionMetadata, c as CheckpointSessionOptions, R as RunCollectorPipelineArgs, d as captureCheckpoint, e as checkpointSlug, f as collectPageTitle, g as createCheckpointSession, h as getBuiltinCollectors, r as registerBuiltinCollector, i as registerBuiltinCollectors, j as resolveCollectors, k as runCollectorPipeline, l as runCollectorSetup, m as runCollectorTeardown, s as sanitizeSegment, n as settlePage, w as warn } from './core-CZvnc0rE.js';
1
+ import { T as TestCheckpointConfig, C as CheckpointConfig, e as CheckpointManifest, f as CheckpointOptions, g as CheckpointRecord, c as CheckpointCollector, a as ReportGenerator, R as RunRecord, B as BoundingBox } from './types-wX4eB9mb.js';
2
+ export { A as AriaSnapshotCollectorData, i as ArticleDefinition, j as ArticleMetadata, k as AxeCollectorData, l as CollectorArtifact, d as CollectorConfig, m as CollectorContext, n as CollectorOptions, o as CollectorResult, p as ConsoleErrorRecord, D as DomStatsCollectorData, F as FailedRequestRecord, q as FormFieldState, r as FormFieldValue, s as FormsCollectorData, H as HtmlCollectorData, N as NetworkTimingBreakdown, t as NetworkTimingCollectorData, u as NetworkTimingRecord, P as PageMetadata, b as ReportGenerationResults, v as ReportGeneratorContext, w as ReportGeneratorResult, x as ReporterConfig, h as ResolvedCollectorConfig, S as ScreenshotCollectorData, y as StorageCollectorData, z as StorageCookieState, E as StorageEntryState, W as WebVitalMetric, G as WebVitalRating, I as WebVitalsSnapshot } from './types-wX4eB9mb.js';
3
+ export { C as CaptureCheckpointOptions, a as CheckpointSession, b as CheckpointSessionMetadata, c as CheckpointSessionOptions, R as RunCollectorPipelineArgs, d as captureCheckpoint, e as checkpointSlug, f as collectPageTitle, g as createCheckpointSession, h as getBuiltinCollectors, r as registerBuiltinCollector, i as registerBuiltinCollectors, j as resolveCollectors, k as runCollectorPipeline, l as runCollectorSetup, m as runCollectorTeardown, s as sanitizeSegment, n as settlePage, w as warn } from './core-6gyzs35M.js';
4
4
  import * as PlaywrightModule from '@playwright/test';
5
5
  import { TestInfo, Page, TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '@playwright/test';
6
- export { d as dedupeRuns, l as loadRuns, a as registerBuiltinReporter, r as runReporters } from './index-BjYQX_hK.js';
6
+ export { d as dedupeRuns, l as loadRuns, a as registerBuiltinReporter, r as runReporters } from './index-CvcgBzvl.js';
7
7
 
8
8
  type DeviceSurface = 'desktop' | 'mobile';
9
9
  type DeviceProfile = {
package/dist/index.js CHANGED
@@ -1,3 +1,15 @@
1
+ import {
2
+ annotateScreenshot,
3
+ dedupeRuns,
4
+ groupByStory,
5
+ htmlReporter,
6
+ loadRuns,
7
+ markdownReporter,
8
+ mdxReporter,
9
+ orderedCheckpointNames,
10
+ registerBuiltinReporter,
11
+ runReporters
12
+ } from "./chunk-WXZOP7XI.js";
1
13
  import {
2
14
  ariaSnapshotCollector,
3
15
  axeCollector,
@@ -27,19 +39,7 @@ import {
27
39
  storageCollector,
28
40
  warn,
29
41
  webVitalsCollector
30
- } from "./chunk-KG37WSYS.js";
31
- import {
32
- annotateScreenshot,
33
- dedupeRuns,
34
- groupByStory,
35
- htmlReporter,
36
- loadRuns,
37
- markdownReporter,
38
- mdxReporter,
39
- orderedCheckpointNames,
40
- registerBuiltinReporter,
41
- runReporters
42
- } from "./chunk-X5IPL32H.js";
42
+ } from "./chunk-M3BRR3LT.js";
43
43
  import "./chunk-DGUM43GV.js";
44
44
 
45
45
  // src/fixture.ts
@@ -91,11 +91,54 @@ function mergeCollectorOverrides(current, updates) {
91
91
  }
92
92
  function mergeTestConfig(current, update) {
93
93
  const collectors = mergeCollectorOverrides(current?.collectors, update.collectors);
94
+ const article = mergeArticleMetadata(current?.article, update.article);
95
+ const articles = update.articles ? cloneArticleDefinitions(update.articles) : current?.articles ? cloneArticleDefinitions(current.articles) : void 0;
94
96
  return {
95
97
  description: update.description ?? current?.description,
98
+ ...article ? { article } : {},
99
+ ...articles ? { articles } : {},
96
100
  ...collectors ? { collectors } : {}
97
101
  };
98
102
  }
103
+ function mergeArticleMetadata(current, update) {
104
+ if (!current && !update) {
105
+ return void 0;
106
+ }
107
+ const merged = {
108
+ ...current ?? {},
109
+ ...update ?? {}
110
+ };
111
+ if (current?.frontmatter || update?.frontmatter) {
112
+ merged.frontmatter = {
113
+ ...current?.frontmatter ?? {},
114
+ ...update?.frontmatter ?? {}
115
+ };
116
+ }
117
+ return merged;
118
+ }
119
+ function cloneArticleMetadata(article) {
120
+ return {
121
+ ...article,
122
+ ...article.frontmatter ? { frontmatter: { ...article.frontmatter } } : {}
123
+ };
124
+ }
125
+ function cloneArticleDefinition(article) {
126
+ return {
127
+ ...cloneArticleMetadata(article),
128
+ steps: [...article.steps]
129
+ };
130
+ }
131
+ function cloneArticleDefinitions(articles) {
132
+ return articles.map((article) => cloneArticleDefinition(article));
133
+ }
134
+ function syncManifestArticle(manifest, testConfig) {
135
+ if (testConfig?.article) {
136
+ manifest.article = cloneArticleMetadata(testConfig.article);
137
+ }
138
+ if (testConfig?.articles) {
139
+ manifest.articles = cloneArticleDefinitions(testConfig.articles);
140
+ }
141
+ }
99
142
  function manifestEnvironment() {
100
143
  return process.env.PLAYWRIGHT_CHECKPOINT_ENV || process.env.NODE_ENV || "test";
101
144
  }
@@ -172,6 +215,8 @@ function createCheckpoint(globalConfig = {}) {
172
215
  const base = playwright.test;
173
216
  const test2 = base.extend({
174
217
  checkpointManifest: [
218
+ // Playwright fixture callbacks must use object destructuring for the first arg.
219
+ // eslint-disable-next-line no-empty-pattern
175
220
  async ({}, use, testInfo) => {
176
221
  const manifest = createCheckpointManifestRecord(testInfo);
177
222
  try {
@@ -186,6 +231,8 @@ function createCheckpoint(globalConfig = {}) {
186
231
  },
187
232
  { auto: true }
188
233
  ],
234
+ // Playwright fixture callbacks must use object destructuring for the first arg.
235
+ // eslint-disable-next-line no-empty-pattern
189
236
  testCheckpointConfig: async ({}, use) => {
190
237
  let current = null;
191
238
  const controller = {
@@ -201,6 +248,8 @@ function createCheckpoint(globalConfig = {}) {
201
248
  };
202
249
  await use(controller);
203
250
  },
251
+ // Playwright fixture callbacks must use object destructuring for the first arg.
252
+ // eslint-disable-next-line no-empty-pattern
204
253
  deviceProfile: async ({}, use, testInfo) => {
205
254
  await use(createDeviceProfile(testInfo));
206
255
  },
@@ -209,15 +258,20 @@ function createCheckpoint(globalConfig = {}) {
209
258
  outputDir: testInfo.outputPath("checkpoints"),
210
259
  manifestPath: testInfo.outputPath("checkpoint-manifest.json"),
211
260
  manifest: checkpointManifest,
212
- collectors: mergeConfig(globalConfig, testCheckpointConfig.get()),
261
+ collectors: globalConfig.collectors,
262
+ testConfig: () => testCheckpointConfig.get(),
213
263
  custom: globalConfig.custom,
214
264
  redact: globalConfig.redact,
215
265
  testInfo,
216
266
  adjustTimeout: createAdjustTimeout(testInfo)
217
267
  });
218
268
  try {
219
- await use((name, options = {}) => session.checkpoint(name, options));
269
+ await use((name, options = {}) => {
270
+ syncManifestArticle(checkpointManifest, testCheckpointConfig.get());
271
+ return session.checkpoint(name, options);
272
+ });
220
273
  } finally {
274
+ syncManifestArticle(checkpointManifest, testCheckpointConfig.get());
221
275
  await session.finalize();
222
276
  }
223
277
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fixture.ts","../src/device-profile.ts","../src/collectors/index.ts","../src/index.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport type * as PlaywrightModule from '@playwright/test';\nimport type {\n Page,\n PlaywrightTestArgs,\n PlaywrightTestOptions,\n PlaywrightWorkerArgs,\n PlaywrightWorkerOptions,\n TestInfo,\n TestType,\n} from '@playwright/test';\nimport {\n createCheckpointSession,\n registerBuiltinCollector,\n resolveCollectors,\n sanitizeSegment,\n settlePage,\n warn,\n} from './core';\nimport { createDeviceProfile, type DeviceProfile } from './device-profile';\nimport type {\n CheckpointConfig,\n CheckpointManifest,\n CheckpointOptions,\n CheckpointRecord,\n CollectorConfig,\n CollectorOptions,\n TestCheckpointConfig,\n} from './types';\n\ntype PlaywrightRuntime = typeof PlaywrightModule;\n\ntype TestCheckpointConfigController = {\n set(config: TestCheckpointConfig): void;\n get(): TestCheckpointConfig | null;\n};\n\ntype CheckpointFixtures = {\n checkpoint: (name: string, options?: CheckpointOptions) => Promise<CheckpointRecord>;\n checkpointManifest: CheckpointManifest;\n testCheckpointConfig: TestCheckpointConfigController;\n deviceProfile: DeviceProfile;\n};\n\nconst require = (() => {\n try {\n return Function('return require')() as NodeRequire;\n } catch {\n return createRequire(path.join(process.cwd(), 'playwright-checkpoint-runtime.cjs'));\n }\n})();\n\nfunction loadPlaywright(): PlaywrightRuntime {\n return require('@playwright/test') as PlaywrightRuntime;\n}\n\nfunction mergeCollectorOverrides(\n current: Partial<Record<string, boolean | CollectorOptions>> | undefined,\n updates: Partial<Record<string, boolean | CollectorOptions>> | undefined,\n): Partial<Record<string, boolean | CollectorOptions>> | undefined {\n if (!current && !updates) {\n return undefined;\n }\n\n const merged: Partial<Record<string, boolean | CollectorOptions>> = {\n ...(current ?? {}),\n };\n\n for (const [name, value] of Object.entries(updates ?? {})) {\n const previous = merged[name];\n\n if (value && typeof value === 'object' && !Array.isArray(value) && previous && typeof previous === 'object' && !Array.isArray(previous)) {\n merged[name] = {\n ...previous,\n ...value,\n };\n continue;\n }\n\n merged[name] = value;\n }\n\n return merged;\n}\n\nfunction mergeTestConfig(current: TestCheckpointConfig | null, update: TestCheckpointConfig): TestCheckpointConfig {\n const collectors = mergeCollectorOverrides(current?.collectors, update.collectors);\n\n return {\n description: update.description ?? current?.description,\n ...(collectors ? { collectors } : {}),\n };\n}\n\nfunction manifestEnvironment(): string {\n return process.env.PLAYWRIGHT_CHECKPOINT_ENV || process.env.NODE_ENV || 'test';\n}\n\nfunction explicitTestTags(testInfo: TestInfo): string[] {\n return (((testInfo as TestInfo & { tags?: string[] }).tags ?? []) as string[]).map((tag) => tag.toLowerCase());\n}\n\nexport function titleParts(testInfo: TestInfo): string[] {\n const maybeTitlePath = (testInfo as { titlePath?: unknown }).titlePath;\n return typeof maybeTitlePath === 'function' ? maybeTitlePath.call(testInfo) : [testInfo.title];\n}\n\nexport function collectTags(parts: string[]): Set<string> {\n const tags = new Set<string>();\n\n for (const part of parts) {\n for (const token of part.match(/@[a-z0-9-]+/gi) || []) {\n tags.add(token.toLowerCase());\n }\n }\n\n return tags;\n}\n\nexport function manifestTags(testInfo: TestInfo): string[] {\n return Array.from(new Set([...explicitTestTags(testInfo), ...collectTags(titleParts(testInfo))]));\n}\n\nexport function createCheckpointManifestRecord(testInfo: TestInfo): CheckpointManifest {\n return {\n environment: manifestEnvironment(),\n project: testInfo.project.name,\n testId: testInfo.testId,\n title: testInfo.title,\n tags: manifestTags(testInfo),\n startedAt: new Date().toISOString(),\n checkpoints: [],\n };\n}\n\nexport async function writeCheckpointManifest(testInfo: TestInfo, manifest: CheckpointManifest): Promise<string> {\n const manifestPath = testInfo.outputPath('checkpoint-manifest.json');\n await fs.mkdir(path.dirname(manifestPath), { recursive: true });\n await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`, 'utf8');\n return manifestPath;\n}\n\nfunction createAdjustTimeout(testInfo: TestInfo): (ms: number) => void {\n return (ms: number) => {\n if (ms > 0 && typeof testInfo.setTimeout === 'function') {\n testInfo.setTimeout(testInfo.timeout + ms);\n }\n };\n}\n\nfunction mergeConfig(\n globalConfig: CheckpointConfig = {},\n testConfig: TestCheckpointConfig | null,\n): Partial<Record<string, boolean | CollectorConfig>> | undefined {\n return mergeCollectorOverrides(\n globalConfig.collectors,\n testConfig?.collectors,\n ) as Partial<Record<string, boolean | CollectorConfig>> | undefined;\n}\n\nexport async function captureCheckpointRecord(args: {\n globalConfig?: CheckpointConfig;\n page: Page;\n testInfo: TestInfo;\n checkpointManifest: CheckpointManifest;\n testConfig?: TestCheckpointConfig | null;\n name: string;\n options?: CheckpointOptions;\n}): Promise<CheckpointRecord> {\n const globalConfig = args.globalConfig ?? {};\n const session = await createCheckpointSession(args.page, {\n outputDir: args.testInfo.outputPath('checkpoints'),\n manifestPath: args.testInfo.outputPath('checkpoint-manifest.json'),\n manifest: args.checkpointManifest,\n collectors: mergeConfig(globalConfig, args.testConfig ?? null),\n custom: globalConfig.custom,\n redact: globalConfig.redact,\n testInfo: args.testInfo,\n adjustTimeout: createAdjustTimeout(args.testInfo),\n });\n\n try {\n return await session.checkpoint(args.name, args.options);\n } finally {\n await session.finalize();\n }\n}\n\nexport function createCheckpoint(globalConfig: CheckpointConfig = {}): {\n test: TestType<PlaywrightTestArgs & PlaywrightTestOptions & CheckpointFixtures, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;\n} {\n const playwright = loadPlaywright();\n const base = playwright.test as TestType<\n PlaywrightTestArgs & PlaywrightTestOptions,\n PlaywrightWorkerArgs & PlaywrightWorkerOptions\n >;\n\n const test = base.extend<CheckpointFixtures>({\n checkpointManifest: [\n async ({}, use, testInfo) => {\n const manifest = createCheckpointManifestRecord(testInfo);\n\n try {\n await use(manifest);\n } finally {\n try {\n await writeCheckpointManifest(testInfo, manifest);\n } catch (error) {\n warn(`Failed to write checkpoint manifest for test \"${testInfo.title}\".`, error);\n }\n }\n },\n { auto: true },\n ],\n\n testCheckpointConfig: async ({}, use) => {\n let current: TestCheckpointConfig | null = null;\n\n const controller: TestCheckpointConfigController = {\n set(config) {\n current = mergeTestConfig(current, config);\n },\n get() {\n return current\n ? {\n ...current,\n ...(current.collectors ? { collectors: mergeCollectorOverrides(undefined, current.collectors) } : {}),\n }\n : null;\n },\n };\n\n await use(controller);\n },\n\n deviceProfile: async ({}, use, testInfo) => {\n await use(createDeviceProfile(testInfo));\n },\n\n checkpoint: async ({ page, checkpointManifest, testCheckpointConfig }, use, testInfo) => {\n const session = await createCheckpointSession(page, {\n outputDir: testInfo.outputPath('checkpoints'),\n manifestPath: testInfo.outputPath('checkpoint-manifest.json'),\n manifest: checkpointManifest,\n collectors: mergeConfig(globalConfig, testCheckpointConfig.get()),\n custom: globalConfig.custom,\n redact: globalConfig.redact,\n testInfo,\n adjustTimeout: createAdjustTimeout(testInfo),\n });\n\n try {\n await use((name, options = {}) => session.checkpoint(name, options));\n } finally {\n await session.finalize();\n }\n },\n });\n\n return { test };\n}\n\nexport const expect = loadPlaywright().expect;\nexport const { test } = createCheckpoint();\nexport { createCheckpointSession, createDeviceProfile, registerBuiltinCollector, resolveCollectors, sanitizeSegment, settlePage, warn };\nexport type { DeviceProfile, TestCheckpointConfigController };\n","import type { TestInfo } from '@playwright/test';\n\nexport type DeviceSurface = 'desktop' | 'mobile';\n\nexport type DeviceProfile = {\n name: string;\n isMobile: boolean;\n surface: DeviceSurface;\n};\n\nexport function createDeviceProfile(testInfo: TestInfo): DeviceProfile {\n const use = testInfo.project.use as { isMobile?: boolean } | undefined;\n const isMobile = Boolean(use?.isMobile);\n\n return {\n name: testInfo.project.name,\n isMobile,\n surface: isMobile ? 'mobile' : 'desktop',\n };\n}\n","import { builtinCollectors } from './builtin-collectors';\nimport { registerBuiltinCollectors } from './registry';\n\nregisterBuiltinCollectors(builtinCollectors);\n\nexport { registerBuiltinCollector, registerBuiltinCollectors, getBuiltinCollectors } from './registry';\nexport { screenshotCollector } from './screenshot';\nexport { htmlCollector } from './html';\nexport { axeCollector, setAxeLoaderForTests } from './axe';\nexport { webVitalsCollector } from './web-vitals';\nexport { consoleCollector } from './console';\nexport { networkCollector } from './network';\nexport { metadataCollector } from './metadata';\nexport { ariaSnapshotCollector } from './aria-snapshot';\nexport { domStatsCollector } from './dom-stats';\nexport { formsCollector } from './forms';\nexport { storageCollector } from './storage';\nexport { networkTimingCollector } from './network-timing';\nexport type { CheckpointCollector } from '../types';\n","export type * from './types';\nexport * from './core';\nexport * from './fixture';\nexport { type CheckpointCollector } from './types';\nexport { type ReportGenerator } from './types';\nexport * from './collectors';\nexport * from './report';\n\nexport const VERSION = '0.1.0';\n"],"mappings":"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","names":["require","test"]}
1
+ {"version":3,"sources":["../src/fixture.ts","../src/device-profile.ts","../src/collectors/index.ts","../src/index.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport type * as PlaywrightModule from '@playwright/test';\nimport type {\n Page,\n PlaywrightTestArgs,\n PlaywrightTestOptions,\n PlaywrightWorkerArgs,\n PlaywrightWorkerOptions,\n TestInfo,\n TestType,\n} from '@playwright/test';\nimport {\n createCheckpointSession,\n registerBuiltinCollector,\n resolveCollectors,\n sanitizeSegment,\n settlePage,\n warn,\n} from './core';\nimport { createDeviceProfile, type DeviceProfile } from './device-profile';\nimport type {\n ArticleDefinition,\n ArticleMetadata,\n CheckpointConfig,\n CheckpointManifest,\n CheckpointOptions,\n CheckpointRecord,\n CollectorConfig,\n CollectorOptions,\n TestCheckpointConfig,\n} from './types';\n\ntype PlaywrightRuntime = typeof PlaywrightModule;\n\ntype TestCheckpointConfigController = {\n set(config: TestCheckpointConfig): void;\n get(): TestCheckpointConfig | null;\n};\n\ntype CheckpointFixtures = {\n checkpoint: (name: string, options?: CheckpointOptions) => Promise<CheckpointRecord>;\n checkpointManifest: CheckpointManifest;\n testCheckpointConfig: TestCheckpointConfigController;\n deviceProfile: DeviceProfile;\n};\n\nconst require = (() => {\n try {\n return Function('return require')() as NodeRequire;\n } catch {\n return createRequire(path.join(process.cwd(), 'playwright-checkpoint-runtime.cjs'));\n }\n})();\n\nfunction loadPlaywright(): PlaywrightRuntime {\n return require('@playwright/test') as PlaywrightRuntime;\n}\n\nfunction mergeCollectorOverrides(\n current: Partial<Record<string, boolean | CollectorOptions>> | undefined,\n updates: Partial<Record<string, boolean | CollectorOptions>> | undefined,\n): Partial<Record<string, boolean | CollectorOptions>> | undefined {\n if (!current && !updates) {\n return undefined;\n }\n\n const merged: Partial<Record<string, boolean | CollectorOptions>> = {\n ...(current ?? {}),\n };\n\n for (const [name, value] of Object.entries(updates ?? {})) {\n const previous = merged[name];\n\n if (value && typeof value === 'object' && !Array.isArray(value) && previous && typeof previous === 'object' && !Array.isArray(previous)) {\n merged[name] = {\n ...previous,\n ...value,\n };\n continue;\n }\n\n merged[name] = value;\n }\n\n return merged;\n}\n\nfunction mergeTestConfig(current: TestCheckpointConfig | null, update: TestCheckpointConfig): TestCheckpointConfig {\n const collectors = mergeCollectorOverrides(current?.collectors, update.collectors);\n const article = mergeArticleMetadata(current?.article, update.article);\n const articles = update.articles ? cloneArticleDefinitions(update.articles) : current?.articles ? cloneArticleDefinitions(current.articles) : undefined;\n\n return {\n description: update.description ?? current?.description,\n ...(article ? { article } : {}),\n ...(articles ? { articles } : {}),\n ...(collectors ? { collectors } : {}),\n };\n}\n\nfunction mergeArticleMetadata(\n current: ArticleMetadata | undefined,\n update: ArticleMetadata | undefined,\n): ArticleMetadata | undefined {\n if (!current && !update) {\n return undefined;\n }\n\n const merged: ArticleMetadata = {\n ...(current ?? {}),\n ...(update ?? {}),\n };\n\n if (current?.frontmatter || update?.frontmatter) {\n merged.frontmatter = {\n ...(current?.frontmatter ?? {}),\n ...(update?.frontmatter ?? {}),\n };\n }\n\n return merged;\n}\n\nfunction cloneArticleMetadata(article: ArticleMetadata): ArticleMetadata {\n return {\n ...article,\n ...(article.frontmatter ? { frontmatter: { ...article.frontmatter } } : {}),\n };\n}\n\nfunction cloneArticleDefinition(article: ArticleDefinition): ArticleDefinition {\n return {\n ...cloneArticleMetadata(article),\n steps: [...article.steps],\n };\n}\n\nfunction cloneArticleDefinitions(articles: ArticleDefinition[]): ArticleDefinition[] {\n return articles.map((article) => cloneArticleDefinition(article));\n}\n\nfunction syncManifestArticle(manifest: CheckpointManifest, testConfig: TestCheckpointConfig | null): void {\n if (testConfig?.article) {\n manifest.article = cloneArticleMetadata(testConfig.article);\n }\n\n if (testConfig?.articles) {\n manifest.articles = cloneArticleDefinitions(testConfig.articles);\n }\n}\n\nfunction manifestEnvironment(): string {\n return process.env.PLAYWRIGHT_CHECKPOINT_ENV || process.env.NODE_ENV || 'test';\n}\n\nfunction explicitTestTags(testInfo: TestInfo): string[] {\n return (((testInfo as TestInfo & { tags?: string[] }).tags ?? []) as string[]).map((tag) => tag.toLowerCase());\n}\n\nexport function titleParts(testInfo: TestInfo): string[] {\n const maybeTitlePath = (testInfo as { titlePath?: unknown }).titlePath;\n return typeof maybeTitlePath === 'function' ? maybeTitlePath.call(testInfo) : [testInfo.title];\n}\n\nexport function collectTags(parts: string[]): Set<string> {\n const tags = new Set<string>();\n\n for (const part of parts) {\n for (const token of part.match(/@[a-z0-9-]+/gi) || []) {\n tags.add(token.toLowerCase());\n }\n }\n\n return tags;\n}\n\nexport function manifestTags(testInfo: TestInfo): string[] {\n return Array.from(new Set([...explicitTestTags(testInfo), ...collectTags(titleParts(testInfo))]));\n}\n\nexport function createCheckpointManifestRecord(testInfo: TestInfo): CheckpointManifest {\n return {\n environment: manifestEnvironment(),\n project: testInfo.project.name,\n testId: testInfo.testId,\n title: testInfo.title,\n tags: manifestTags(testInfo),\n startedAt: new Date().toISOString(),\n checkpoints: [],\n };\n}\n\nexport async function writeCheckpointManifest(testInfo: TestInfo, manifest: CheckpointManifest): Promise<string> {\n const manifestPath = testInfo.outputPath('checkpoint-manifest.json');\n await fs.mkdir(path.dirname(manifestPath), { recursive: true });\n await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\\n`, 'utf8');\n return manifestPath;\n}\n\nfunction createAdjustTimeout(testInfo: TestInfo): (ms: number) => void {\n return (ms: number) => {\n if (ms > 0 && typeof testInfo.setTimeout === 'function') {\n testInfo.setTimeout(testInfo.timeout + ms);\n }\n };\n}\n\nfunction mergeConfig(\n globalConfig: CheckpointConfig = {},\n testConfig: TestCheckpointConfig | null,\n): Partial<Record<string, boolean | CollectorConfig>> | undefined {\n return mergeCollectorOverrides(\n globalConfig.collectors,\n testConfig?.collectors,\n ) as Partial<Record<string, boolean | CollectorConfig>> | undefined;\n}\n\nexport async function captureCheckpointRecord(args: {\n globalConfig?: CheckpointConfig;\n page: Page;\n testInfo: TestInfo;\n checkpointManifest: CheckpointManifest;\n testConfig?: TestCheckpointConfig | null;\n name: string;\n options?: CheckpointOptions;\n}): Promise<CheckpointRecord> {\n const globalConfig = args.globalConfig ?? {};\n const session = await createCheckpointSession(args.page, {\n outputDir: args.testInfo.outputPath('checkpoints'),\n manifestPath: args.testInfo.outputPath('checkpoint-manifest.json'),\n manifest: args.checkpointManifest,\n collectors: mergeConfig(globalConfig, args.testConfig ?? null),\n custom: globalConfig.custom,\n redact: globalConfig.redact,\n testInfo: args.testInfo,\n adjustTimeout: createAdjustTimeout(args.testInfo),\n });\n\n try {\n return await session.checkpoint(args.name, args.options);\n } finally {\n await session.finalize();\n }\n}\n\nexport function createCheckpoint(globalConfig: CheckpointConfig = {}): {\n test: TestType<PlaywrightTestArgs & PlaywrightTestOptions & CheckpointFixtures, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;\n} {\n const playwright = loadPlaywright();\n const base = playwright.test as TestType<\n PlaywrightTestArgs & PlaywrightTestOptions,\n PlaywrightWorkerArgs & PlaywrightWorkerOptions\n >;\n\n const test = base.extend<CheckpointFixtures>({\n checkpointManifest: [\n // Playwright fixture callbacks must use object destructuring for the first arg.\n // eslint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n const manifest = createCheckpointManifestRecord(testInfo);\n\n try {\n await use(manifest);\n } finally {\n try {\n await writeCheckpointManifest(testInfo, manifest);\n } catch (error) {\n warn(`Failed to write checkpoint manifest for test \"${testInfo.title}\".`, error);\n }\n }\n },\n { auto: true },\n ],\n\n // Playwright fixture callbacks must use object destructuring for the first arg.\n // eslint-disable-next-line no-empty-pattern\n testCheckpointConfig: async ({}, use) => {\n let current: TestCheckpointConfig | null = null;\n\n const controller: TestCheckpointConfigController = {\n set(config) {\n current = mergeTestConfig(current, config);\n },\n get() {\n return current\n ? {\n ...current,\n ...(current.collectors ? { collectors: mergeCollectorOverrides(undefined, current.collectors) } : {}),\n }\n : null;\n },\n };\n\n await use(controller);\n },\n\n // Playwright fixture callbacks must use object destructuring for the first arg.\n // eslint-disable-next-line no-empty-pattern\n deviceProfile: async ({}, use, testInfo) => {\n await use(createDeviceProfile(testInfo));\n },\n\n checkpoint: async ({ page, checkpointManifest, testCheckpointConfig }, use, testInfo) => {\n const session = await createCheckpointSession(page, {\n outputDir: testInfo.outputPath('checkpoints'),\n manifestPath: testInfo.outputPath('checkpoint-manifest.json'),\n manifest: checkpointManifest,\n collectors: globalConfig.collectors,\n testConfig: () => testCheckpointConfig.get(),\n custom: globalConfig.custom,\n redact: globalConfig.redact,\n testInfo,\n adjustTimeout: createAdjustTimeout(testInfo),\n });\n\n try {\n await use((name, options = {}) => {\n syncManifestArticle(checkpointManifest, testCheckpointConfig.get());\n return session.checkpoint(name, options);\n });\n } finally {\n syncManifestArticle(checkpointManifest, testCheckpointConfig.get());\n await session.finalize();\n }\n },\n });\n\n return { test };\n}\n\nexport const expect = loadPlaywright().expect;\nexport const { test } = createCheckpoint();\nexport { createCheckpointSession, createDeviceProfile, registerBuiltinCollector, resolveCollectors, sanitizeSegment, settlePage, warn };\nexport type { DeviceProfile, TestCheckpointConfigController };\n","import type { TestInfo } from '@playwright/test';\n\nexport type DeviceSurface = 'desktop' | 'mobile';\n\nexport type DeviceProfile = {\n name: string;\n isMobile: boolean;\n surface: DeviceSurface;\n};\n\nexport function createDeviceProfile(testInfo: TestInfo): DeviceProfile {\n const use = testInfo.project.use as { isMobile?: boolean } | undefined;\n const isMobile = Boolean(use?.isMobile);\n\n return {\n name: testInfo.project.name,\n isMobile,\n surface: isMobile ? 'mobile' : 'desktop',\n };\n}\n","import { builtinCollectors } from './builtin-collectors';\nimport { registerBuiltinCollectors } from './registry';\n\nregisterBuiltinCollectors(builtinCollectors);\n\nexport { registerBuiltinCollector, registerBuiltinCollectors, getBuiltinCollectors } from './registry';\nexport { screenshotCollector } from './screenshot';\nexport { htmlCollector } from './html';\nexport { axeCollector, setAxeLoaderForTests } from './axe';\nexport { webVitalsCollector } from './web-vitals';\nexport { consoleCollector } from './console';\nexport { networkCollector } from './network';\nexport { metadataCollector } from './metadata';\nexport { ariaSnapshotCollector } from './aria-snapshot';\nexport { domStatsCollector } from './dom-stats';\nexport { formsCollector } from './forms';\nexport { storageCollector } from './storage';\nexport { networkTimingCollector } from './network-timing';\nexport type { CheckpointCollector } from '../types';\n","export type * from './types';\nexport * from './core';\nexport * from './fixture';\nexport { type CheckpointCollector } from './types';\nexport { type ReportGenerator } from './types';\nexport * from './collectors';\nexport * from './report';\n\nexport const VERSION = '0.1.0';\n"],"mappings":"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","names":["require","test"]}
@@ -2379,6 +2379,28 @@ function stripTags(value) {
2379
2379
  const stripped = value.replace(/\s+@[a-z0-9-]+/gi, " ").replace(/\s+/g, " ").trim();
2380
2380
  return stripped || value.trim() || "Untitled story";
2381
2381
  }
2382
+ function articleTitle(run) {
2383
+ const override = run.article?.title?.trim();
2384
+ return override || stripTags(run.title);
2385
+ }
2386
+ function articleDescription(run) {
2387
+ const description = run.article?.description?.trim();
2388
+ return description ? description : null;
2389
+ }
2390
+ function articleSlug(run) {
2391
+ const override = run.article?.slug?.trim();
2392
+ return slugify2(override || stripTags(run.title));
2393
+ }
2394
+ function uniqueArticleSlug(baseSlug, usedSlugs) {
2395
+ if (!usedSlugs.has(baseSlug)) {
2396
+ return baseSlug;
2397
+ }
2398
+ let index = 1;
2399
+ while (usedSlugs.has(`${baseSlug}-${index}`)) {
2400
+ index += 1;
2401
+ }
2402
+ return `${baseSlug}-${index}`;
2403
+ }
2382
2404
  function normalizeConfig(config) {
2383
2405
  return {
2384
2406
  storiesDir: typeof config.storiesDir === "string" ? config.storiesDir : ".",
@@ -2389,7 +2411,8 @@ function normalizeConfig(config) {
2389
2411
  footer: typeof config.footer === "string" ? config.footer : void 0,
2390
2412
  frontmatter: config.frontmatter === true || config.frontmatter === false || config.frontmatter != null && typeof config.frontmatter === "object" && !Array.isArray(config.frontmatter) ? config.frontmatter : false,
2391
2413
  imagePathPrefix: typeof config.imagePathPrefix === "string" ? config.imagePathPrefix : void 0,
2392
- copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true
2414
+ copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true,
2415
+ requireExplicitStep: typeof config.requireExplicitStep === "boolean" ? config.requireExplicitStep : false
2393
2416
  };
2394
2417
  }
2395
2418
  function normalizeTags(tags) {
@@ -2401,6 +2424,9 @@ function shouldIncludeRun(run, config) {
2401
2424
  const runTags = new Set(normalizeTags(run.tags));
2402
2425
  return includeTags.some((tag) => runTags.has(tag));
2403
2426
  }
2427
+ if (run.articles) {
2428
+ return true;
2429
+ }
2404
2430
  return run.checkpoints.some((checkpoint) => {
2405
2431
  const hasDescription = typeof checkpoint.description === "string" && checkpoint.description.trim().length > 0;
2406
2432
  return hasDescription || typeof checkpoint.step === "number";
@@ -2519,17 +2545,22 @@ async function materializeScreenshot(args) {
2519
2545
  if (!sourcePath) {
2520
2546
  return null;
2521
2547
  }
2548
+ const cachedTargetPath = args.screenshotCopies?.get(sourcePath);
2549
+ if (cachedTargetPath) {
2550
+ return rewriteImagePath(args.markdownFile, cachedTargetPath, args.outputDir, args.config.imagePathPrefix);
2551
+ }
2522
2552
  const extension = import_node_path15.default.extname(sourcePath) || ".png";
2523
2553
  const targetPath = import_node_path15.default.join(
2524
2554
  args.outputDir,
2525
2555
  args.config.screenshotsDir ?? "screenshots",
2526
- args.storySlug,
2527
- `${String(args.stepOrder).padStart(2, "0")}-${slugify2(args.checkpoint.name)}${extension}`
2556
+ args.screenshotDirSlug,
2557
+ `${args.screenshotFileSlug}${extension}`
2528
2558
  );
2529
2559
  try {
2530
2560
  if (args.config.copyScreenshots !== false) {
2531
2561
  await import_promises14.default.mkdir(import_node_path15.default.dirname(targetPath), { recursive: true });
2532
2562
  await import_promises14.default.copyFile(sourcePath, targetPath);
2563
+ args.screenshotCopies?.set(sourcePath, targetPath);
2533
2564
  args.writtenFiles.add(targetPath);
2534
2565
  return rewriteImagePath(args.markdownFile, targetPath, args.outputDir, args.config.imagePathPrefix);
2535
2566
  }
@@ -2549,10 +2580,31 @@ function orderedCheckpoints(checkpoints) {
2549
2580
  });
2550
2581
  }
2551
2582
  async function buildSteps(args) {
2552
- const checkpoints = orderedCheckpoints(args.run.checkpoints);
2583
+ let checkpoints;
2584
+ if (args.stepNames && args.stepNames.length > 0) {
2585
+ const byName = /* @__PURE__ */ new Map();
2586
+ for (const checkpoint of args.run.checkpoints) {
2587
+ if (byName.has(checkpoint.name)) {
2588
+ warn(`Duplicate checkpoint name "${checkpoint.name}" in "${args.run.title}". Using the latest capture for article generation.`);
2589
+ }
2590
+ byName.set(checkpoint.name, checkpoint);
2591
+ }
2592
+ checkpoints = args.stepNames.map((stepName) => {
2593
+ const checkpoint = byName.get(stepName);
2594
+ if (!checkpoint) {
2595
+ warn(`Markdown article step "${stepName}" was not captured in "${args.run.title}". Skipping step.`);
2596
+ return null;
2597
+ }
2598
+ return checkpoint;
2599
+ }).filter((checkpoint) => checkpoint !== null);
2600
+ } else {
2601
+ checkpoints = orderedCheckpoints(args.run.checkpoints).filter(
2602
+ (checkpoint) => !args.config.requireExplicitStep || typeof checkpoint.step === "number"
2603
+ );
2604
+ }
2553
2605
  const steps = [];
2554
2606
  for (const [index, checkpoint] of checkpoints.entries()) {
2555
- const order = typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
2607
+ const order = args.stepNames ? index + 1 : typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
2556
2608
  steps.push({
2557
2609
  checkpoint,
2558
2610
  order,
@@ -2561,12 +2613,13 @@ async function buildSteps(args) {
2561
2613
  imagePath: await materializeScreenshot({
2562
2614
  run: args.run,
2563
2615
  checkpoint,
2564
- storySlug: args.storySlug,
2565
- stepOrder: order,
2616
+ screenshotDirSlug: args.screenshotDirSlug,
2617
+ screenshotFileSlug: args.stepNames ? checkpoint.slug : `${String(order).padStart(2, "0")}-${slugify2(checkpoint.name)}`,
2566
2618
  outputDir: args.outputDir,
2567
2619
  markdownFile: args.markdownFile,
2568
2620
  config: args.config,
2569
- writtenFiles: args.writtenFiles
2621
+ writtenFiles: args.writtenFiles,
2622
+ screenshotCopies: args.screenshotCopies
2570
2623
  }),
2571
2624
  urlLabel: urlLabel(checkpoint.url),
2572
2625
  breadcrumbLabel: breadcrumbLabel(checkpoint.url),
@@ -2577,13 +2630,14 @@ async function buildSteps(args) {
2577
2630
  }
2578
2631
  function renderMarkdown(args) {
2579
2632
  const frontmatterFields = args.config.frontmatter === true || typeof args.config.frontmatter === "object" ? {
2580
- title: args.title,
2581
2633
  project: args.run.project,
2582
- testId: args.run.testId,
2583
2634
  tags: args.run.tags,
2635
+ ...args.config.frontmatter && typeof args.config.frontmatter === "object" ? args.config.frontmatter : {},
2636
+ ...args.article?.frontmatter ?? {},
2637
+ testId: args.run.testId,
2584
2638
  startedAt: args.run.startedAt,
2585
2639
  generatedAt: args.generatedAt,
2586
- ...args.config.frontmatter && typeof args.config.frontmatter === "object" ? args.config.frontmatter : {}
2640
+ title: args.title
2587
2641
  } : null;
2588
2642
  const sections = args.steps.map((step) => {
2589
2643
  const lines = [`## Step ${step.order}: ${step.heading}`, ""];
@@ -2603,6 +2657,7 @@ function renderMarkdown(args) {
2603
2657
  const parts = [
2604
2658
  frontmatterFields ? serializeFrontmatter(frontmatterFields) : "",
2605
2659
  `# ${args.title}`,
2660
+ args.description?.trim() ?? "",
2606
2661
  args.config.header ? args.config.header.trim() : "",
2607
2662
  sections,
2608
2663
  args.config.footer ? args.config.footer.trim() : ""
@@ -2610,6 +2665,42 @@ function renderMarkdown(args) {
2610
2665
  return `${parts.join("\n\n")}
2611
2666
  `;
2612
2667
  }
2668
+ function resolveArticles(run) {
2669
+ const multiArticles = (run.articles ?? []).filter((article) => Array.isArray(article.steps));
2670
+ if (run.articles && multiArticles.length === 0) {
2671
+ warn(`Markdown reporter received an empty articles array for "${run.title}". Falling back to the default single-article output.`);
2672
+ }
2673
+ if (multiArticles.length === 0) {
2674
+ return [
2675
+ {
2676
+ title: articleTitle(run),
2677
+ description: articleDescription(run),
2678
+ slug: articleSlug(run),
2679
+ metadata: run.article,
2680
+ screenshotDirSlug: articleSlug(run)
2681
+ }
2682
+ ];
2683
+ }
2684
+ const usedSlugs = /* @__PURE__ */ new Set();
2685
+ const screenshotDirSlug = slugify2(stripTags(run.title));
2686
+ return multiArticles.map((article, index) => {
2687
+ const fallbackSlug = `${screenshotDirSlug}-${index + 1}`;
2688
+ const baseSlug = slugify2(article.slug?.trim() || fallbackSlug);
2689
+ const uniqueSlug = uniqueArticleSlug(baseSlug, usedSlugs);
2690
+ if (uniqueSlug !== baseSlug) {
2691
+ warn(`Markdown article slug collision for "${article.title ?? run.title}" resolved as "${uniqueSlug}".`);
2692
+ }
2693
+ usedSlugs.add(uniqueSlug);
2694
+ return {
2695
+ title: article.title?.trim() || stripTags(run.title),
2696
+ description: article.description?.trim() || null,
2697
+ slug: uniqueSlug,
2698
+ metadata: article,
2699
+ stepNames: [...article.steps],
2700
+ screenshotDirSlug
2701
+ };
2702
+ });
2703
+ }
2613
2704
  var markdownReporter = {
2614
2705
  name: "markdown",
2615
2706
  description: "Generates one Markdown help article per captured story.",
@@ -2621,37 +2712,56 @@ var markdownReporter = {
2621
2712
  const stories = groupByStory(context.runs);
2622
2713
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2623
2714
  const writtenFiles = /* @__PURE__ */ new Set();
2715
+ const usedStorySlugs = /* @__PURE__ */ new Set();
2716
+ const screenshotCopies = /* @__PURE__ */ new Map();
2624
2717
  let articleCount = 0;
2625
2718
  for (const [storyTitle, runs] of stories) {
2626
2719
  const primaryRun = choosePrimaryRun(runs, config.preferredProject);
2627
2720
  if (!primaryRun || !shouldIncludeRun(primaryRun, config)) {
2628
2721
  continue;
2629
2722
  }
2630
- const title = stripTags(storyTitle);
2631
- const storySlug = slugify2(title);
2632
- const markdownFile = import_node_path15.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.md`);
2633
- const steps = await buildSteps({
2634
- run: primaryRun,
2635
- storySlug,
2636
- outputDir: context.outputDir,
2637
- markdownFile,
2638
- config,
2639
- writtenFiles
2640
- });
2641
- await import_promises14.default.mkdir(import_node_path15.default.dirname(markdownFile), { recursive: true });
2642
- await import_promises14.default.writeFile(
2643
- markdownFile,
2644
- renderMarkdown({
2645
- title,
2646
- steps,
2723
+ for (const article of resolveArticles(primaryRun)) {
2724
+ let storySlug = article.slug;
2725
+ if (usedStorySlugs.has(storySlug)) {
2726
+ let index = 2;
2727
+ while (usedStorySlugs.has(`${article.slug}-${index}`)) {
2728
+ index += 1;
2729
+ }
2730
+ storySlug = `${article.slug}-${index}`;
2731
+ warn(`Markdown article slug collision for "${article.title || storyTitle}" resolved as "${storySlug}".`);
2732
+ }
2733
+ usedStorySlugs.add(storySlug);
2734
+ const markdownFile = import_node_path15.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.md`);
2735
+ const steps = await buildSteps({
2647
2736
  run: primaryRun,
2737
+ stepNames: article.stepNames,
2738
+ screenshotDirSlug: article.screenshotDirSlug,
2739
+ outputDir: context.outputDir,
2740
+ markdownFile,
2648
2741
  config,
2649
- generatedAt
2650
- }),
2651
- "utf8"
2652
- );
2653
- writtenFiles.add(markdownFile);
2654
- articleCount += 1;
2742
+ writtenFiles,
2743
+ screenshotCopies
2744
+ });
2745
+ if (steps.length === 0) {
2746
+ continue;
2747
+ }
2748
+ await import_promises14.default.mkdir(import_node_path15.default.dirname(markdownFile), { recursive: true });
2749
+ await import_promises14.default.writeFile(
2750
+ markdownFile,
2751
+ renderMarkdown({
2752
+ title: article.title,
2753
+ description: article.description,
2754
+ steps,
2755
+ run: primaryRun,
2756
+ article: article.metadata,
2757
+ config,
2758
+ generatedAt
2759
+ }),
2760
+ "utf8"
2761
+ );
2762
+ writtenFiles.add(markdownFile);
2763
+ articleCount += 1;
2764
+ }
2655
2765
  }
2656
2766
  return {
2657
2767
  files: [...writtenFiles],
@@ -3064,6 +3174,8 @@ function toRunRecord(manifest, sourceManifestPath) {
3064
3174
  project: manifest.project,
3065
3175
  testId: manifest.testId,
3066
3176
  title: manifest.title,
3177
+ ...manifest.article ? { article: manifest.article } : {},
3178
+ ...manifest.articles ? { articles: manifest.articles } : {},
3067
3179
  tags: manifest.tags,
3068
3180
  startedAt: manifest.startedAt,
3069
3181
  checkpoints: manifest.checkpoints
@@ -3075,6 +3187,8 @@ function toManifest(run) {
3075
3187
  project: run.project,
3076
3188
  testId: run.testId,
3077
3189
  title: run.title,
3190
+ ...run.article ? { article: run.article } : {},
3191
+ ...run.articles ? { articles: run.articles } : {},
3078
3192
  tags: run.tags,
3079
3193
  startedAt: run.startedAt,
3080
3194
  checkpoints: run.checkpoints