@qualflare/playwright 0.2.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.
- package/README.md +21 -6
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/reporter/index.cjs +146 -19
- package/dist/reporter/index.cjs.map +1 -1
- package/dist/reporter/index.d.cts +4 -3
- package/dist/reporter/index.d.ts +4 -3
- package/dist/reporter/index.js +146 -19
- package/dist/reporter/index.js.map +1 -1
- package/dist/{resolve-config-CQe-oDpg.d.cts → resolve-config-T2ChZzAm.d.cts} +59 -0
- package/dist/{resolve-config-CQe-oDpg.d.ts → resolve-config-T2ChZzAm.d.ts} +59 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
7
|
A native Playwright reporter for [Qualflare](https://qualflare.com) — captures test results directly
|
|
8
|
-
from your `playwright test` run: status,
|
|
9
|
-
|
|
8
|
+
from your `playwright test` run: status, per-attempt retry history and flakiness, nested
|
|
9
|
+
`test.step()` trees,
|
|
10
|
+
screenshots, videos, traces, and author-facing metadata (labels, links, tags, priority, custom attachments).
|
|
10
11
|
|
|
11
12
|
The reporter itself makes **no network calls**. It writes a report directory, and
|
|
12
13
|
[`qualflare-cli`](https://github.com/Qualflare/qualflare-cli) uploads it — which is what lets any
|
|
@@ -113,17 +114,31 @@ wrong value cannot fail at test time — the reporter makes no requests — so t
|
|
|
113
114
|
|
|
114
115
|
## Known limitations
|
|
115
116
|
|
|
116
|
-
- **Traces
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
- **Traces need `--upload-artifacts=trace` at collect time.** The zip is copied into `outputDir`,
|
|
118
|
+
but `qf collect` uploads no heavy artifact unless asked — pass `--upload-artifacts=trace` (or
|
|
119
|
+
`video,trace`). Needs `@qualflare/cli` v0.1.20+; older CLIs ignore it.
|
|
119
120
|
- **`pw:api` and `fixture` steps are filtered out by default** (`includeApiSteps`) — a single
|
|
120
121
|
browser test emits hundreds, which buries the steps you actually wrote. A *failed* one is always
|
|
121
122
|
kept.
|
|
122
123
|
- **A stale `outputDir` is refused, not merged** — each report carries a `runId`, and `qf collect`
|
|
123
124
|
errors rather than merging files from two different runs. Needs `@qualflare/cli` v0.1.19+; older
|
|
124
125
|
CLIs merge as before.
|
|
125
|
-
- **`merge-reports` mode is not supported**
|
|
126
|
+
- **`merge-reports` mode is not supported** — use the `outputDir` flow above rather than
|
|
126
127
|
Playwright's `blob` reporter.
|
|
128
|
+
- **Playwright-native `tag` needs 1.42+** while the peer floor is 1.40 — on 1.40/1.41 the
|
|
129
|
+
native tag array is not read; `qualflare.tag()` works throughout.
|
|
130
|
+
- **`parameter()` outside a step is not masked** — `masked` is a display hint for the UI; the
|
|
131
|
+
server never redacts the value, so never put a real secret in one. See
|
|
132
|
+
[`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md#parameter-outside-a-step-has-no-masking).
|
|
133
|
+
- **Attachment caps are two budgets, not one pool** — `maxAttachmentBytes` bounds a single
|
|
134
|
+
attachment and `maxTotalAttachmentBytes` the whole run; anything over either is dropped
|
|
135
|
+
outright rather than truncated. Raising them is the easiest way to push a request past
|
|
136
|
+
`/collect`'s body limit. See
|
|
137
|
+
[`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md#per-case-and-per-attachment-caps-are-independent-not-pooled).
|
|
138
|
+
- **Retries carry per-attempt errors, but everything else is the final attempt** — `Case.attempts`
|
|
139
|
+
records each attempt's status, duration and error; steps, labels, links, tags, priority,
|
|
140
|
+
properties and attachments come from the last attempt only, so an abandoned attempt's step trace
|
|
141
|
+
is discarded rather than replayed alongside the final one.
|
|
127
142
|
|
|
128
143
|
Full details in [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md).
|
|
129
144
|
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var import_test = require("@playwright/test");
|
|
|
31
31
|
// src/shared/constants.ts
|
|
32
32
|
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
33
33
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
34
|
+
var MAX_TRACE_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
34
35
|
|
|
35
36
|
// src/shared/logger.ts
|
|
36
37
|
var PREFIX = "[qualflare-playwright]";
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/runtime/qualflare-api.ts","../src/shared/constants.ts","../src/shared/logger.ts"],"sourcesContent":["import type { ReporterDescription } from '@playwright/test';\n\nimport type { QualflarePlaywrightOptions } from './config/resolve-config.js';\n\nexport { qualflare } from './runtime/qualflare-api.js';\n\nexport type { QualflarePlaywrightOptions, ResolvedReporterConfig } from './config/resolve-config.js';\n\nexport type {\n Attachment,\n Case,\n CasePriority,\n CaseStatus,\n Collect,\n FrameworkCategory,\n Label,\n Link,\n LinkType,\n Metadata,\n NanosecondDuration,\n Parameter,\n Platform,\n Step,\n Suite,\n} from './shared/types.js';\n\n/**\n * Typed helper for registering the reporter.\n *\n * Playwright types a reporter's options as `any` (`ReporterDescription` ends\n * in `[string, any]`), so writing the tuple by hand gives no autocomplete and\n * silently accepts typos. This returns the same tuple with the options\n * checked:\n *\n * ```ts\n * import { defineConfig } from '@playwright/test';\n * import { qualflareReporter } from '@qualflare/playwright';\n *\n * export default defineConfig({\n * reporter: [['list'], qualflareReporter({ environment: 'staging' })],\n * });\n * ```\n */\nexport function qualflareReporter(options: QualflarePlaywrightOptions = {}): ReporterDescription {\n return ['@qualflare/playwright/reporter', options];\n}\n","import { test } from '@playwright/test';\n\nimport { RESERVED_MESSAGE_MEDIA_TYPE } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { CasePriority, LinkType } from '../shared/types.js';\nimport type { RuntimeMessage } from './message-types.js';\n\n/**\n * Ships one structured message from the test process to the reporter.\n *\n * Playwright runs tests in worker processes and reporters in the main\n * process, with no shared memory — the only user-reachable channel back is\n * `testInfo.attach()`. So every `qualflare.*()` call is serialized and\n * attached under a reserved content type; the reporter recognizes that exact\n * type in `onTestEnd`, replays it as a model mutation, and excludes it from\n * real attachment processing. Same trick `@qualflare/cucumberjs` plays with\n * `World.attach()`, for the same reason.\n *\n * `test.info()` throws when called outside a running test (module scope, a\n * `globalSetup`, a stray import). Warn and drop: a metadata call is never\n * worth failing somebody's suite over.\n */\nfunction send(message: RuntimeMessage): void {\n try {\n // The .catch() is not optional. `attach()` returns a promise, and an\n // unhandled rejection TERMINATES the process by default on Node >= 15 —\n // a metadata call would take the user's entire test run down with it.\n // The surrounding try/catch only covers a synchronous throw from\n // `test.info()` (called outside a running test), not this.\n void test\n .info()\n .attach(`qualflare:${message.type}`, {\n body: Buffer.from(JSON.stringify(message), 'utf8'),\n contentType: RESERVED_MESSAGE_MEDIA_TYPE,\n })\n .catch((err: unknown) => {\n logger.warn(`qualflare.${message.type}() could not be recorded: ${(err as Error).message}`);\n });\n } catch {\n logger.warn(`qualflare.${message.type}() was called outside a running test; the call was ignored.`);\n }\n}\n\n/**\n * Author-facing metadata API. Import it in a spec and annotate tests with\n * business context Playwright itself has no concept of:\n *\n * ```ts\n * import { qualflare } from '@qualflare/playwright';\n *\n * test('checks out', async ({ page }) => {\n * qualflare.label('epic', 'Billing');\n * qualflare.link('https://tracker/QF-1', { type: 'issue', name: 'QF-1' });\n * await qualflare.step('pay', async () => { ... });\n * });\n * ```\n */\nexport const qualflare = {\n /** Arbitrary name/value metadata (epic, feature, story, owner, severity). */\n label(name: string, value: string): void {\n send({ type: 'label', name, value });\n },\n\n /** A typed external reference. `type` defaults to `custom`. */\n link(url: string, opts?: { type?: LinkType; name?: string }): void {\n send({ type: 'link', url, linkType: opts?.type, name: opts?.name });\n },\n\n /** One or more free-text tags. */\n tag(...tags: string[]): void {\n send({ type: 'tag', tags });\n },\n\n /** Markdown description shown on the case. */\n description(text: string): void {\n send({ type: 'description', text });\n },\n\n /** Case priority (low | medium | high | critical). */\n priority(value: CasePriority): void {\n send({ type: 'priority', value });\n },\n\n /** A named input. Inside an open `step()` it attaches to that step;\n * outside any step it lands in the case's properties. `masked` is a\n * display hint for the UI only — the server does not redact the value. */\n parameter(name: string, value?: string, opts?: { masked?: boolean }): void {\n send({ type: 'parameter', name, value, masked: opts?.masked });\n },\n\n /** Attach in-memory content. */\n attachment(name: string, content: string, opts?: { encoding?: 'utf8' | 'base64'; mimeType?: string }): void {\n const contentBase64 = opts?.encoding === 'base64' ? content : Buffer.from(content, 'utf8').toString('base64');\n send({ type: 'attachment', name, contentBase64, mimeType: opts?.mimeType });\n },\n\n /** Attach a file from disk. */\n attachmentFromFile(name: string, path: string, opts?: { mimeType?: string }): void {\n send({ type: 'attachment_from_file', name, path, mimeType: opts?.mimeType });\n },\n\n /**\n * Records a named step around `fn`.\n *\n * Unlike the cucumber-js equivalent, this delegates to Playwright's own\n * `test.step()` as well, so the step shows up in Playwright's HTML report\n * and trace viewer in addition to Qualflare — there is no reason to make\n * users choose, and a step that exists in only one of the two is a\n * confusing thing to debug against.\n */\n async step<T>(name: string, fn: () => T | Promise<T>): Promise<T> {\n return test.step(name, async () => {\n send({ type: 'step_start', name, timestamp: Date.now() });\n try {\n const result = await fn();\n send({ type: 'step_stop', status: 'passed', timestamp: Date.now() });\n return result;\n } catch (err) {\n send({\n type: 'step_stop',\n status: 'failed',\n error: err instanceof Error ? err.message : String(err),\n timestamp: Date.now(),\n });\n throw err;\n }\n });\n },\n};\n","/**\n * Shared constants used across the reporter and the author-facing runtime\n * API.\n */\n\n/** Reserved `testInfo.attach()` content type used to smuggle structured\n * `qualflare.*()` calls (label/tag/step/etc.) from test and hook code back to\n * the reporter — the only channel Playwright gives user code back to a\n * running reporter. The reporter recognizes this exact content type and\n * replays the message as a model mutation instead of reporting it as a\n * literal attachment. */\nexport const RESERVED_MESSAGE_MEDIA_TYPE = 'application/vnd.qualflare.message+json';\n\n/** Server-side caps this client should respect defensively (see\n * `api-service/internal/core/domain/launch/launch.go`). */\nexport const MAX_SUITES_PER_LAUNCH = 2000;\nexport const MAX_CASES_PER_SUITE = 5000;\nexport const MAX_STEPS_PER_CASE = 1000;\nexport const MAX_PARAMETERS_PER_STEP = 50;\nexport const MAX_ATTACHMENTS_PER_CASE = 50;\nexport const MAX_LABELS_PER_CASE = 100;\nexport const MAX_LINKS_PER_CASE = 20;\nexport const MAX_TAGS_PER_CASE = 64;\nexport const MAX_TAG_LENGTH = 255;\n\n/** Mirrors `launch.MaxAttachmentUploadFileSize` — the server's hard cap on a\n * single `POST /api/v1/attachments/upload-url` request (video). */\nexport const MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;\n\n/** Client-side SOFT cap on steps recorded per scenario attempt — well under\n * the server's 1000-per-case hard cap (`MAX_STEPS_PER_CASE`). Once hit,\n * further steps within that attempt are dropped (with a one-time warning),\n * not queued and truncated later. */\nexport const MAX_STEPS_PER_TEST_ATTEMPT = 300;\n","/**\n * A minimal logger writing to stderr. Deliberately avoids stdout, since\n * that's typically Playwright's own reporter output stream and shouldn't be\n * polluted with reporter diagnostics.\n */\n\nconst PREFIX = '[qualflare-playwright]';\n\nexport const logger = {\n debug(...args: unknown[]): void {\n console.debug(PREFIX, ...args);\n },\n info(...args: unknown[]): void {\n console.log(PREFIX, ...args);\n },\n warn(...args: unknown[]): void {\n console.warn(PREFIX, ...args);\n },\n error(...args: unknown[]): void {\n console.error(PREFIX, ...args);\n },\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/runtime/qualflare-api.ts","../src/shared/constants.ts","../src/shared/logger.ts"],"sourcesContent":["import type { ReporterDescription } from '@playwright/test';\n\nimport type { QualflarePlaywrightOptions } from './config/resolve-config.js';\n\nexport { qualflare } from './runtime/qualflare-api.js';\n\nexport type { QualflarePlaywrightOptions, ResolvedReporterConfig } from './config/resolve-config.js';\n\nexport type {\n Attachment,\n Case,\n CasePriority,\n CaseStatus,\n Collect,\n FrameworkCategory,\n Label,\n Link,\n LinkType,\n Metadata,\n NanosecondDuration,\n Parameter,\n Platform,\n Step,\n Suite,\n} from './shared/types.js';\n\n/**\n * Typed helper for registering the reporter.\n *\n * Playwright types a reporter's options as `any` (`ReporterDescription` ends\n * in `[string, any]`), so writing the tuple by hand gives no autocomplete and\n * silently accepts typos. This returns the same tuple with the options\n * checked:\n *\n * ```ts\n * import { defineConfig } from '@playwright/test';\n * import { qualflareReporter } from '@qualflare/playwright';\n *\n * export default defineConfig({\n * reporter: [['list'], qualflareReporter({ environment: 'staging' })],\n * });\n * ```\n */\nexport function qualflareReporter(options: QualflarePlaywrightOptions = {}): ReporterDescription {\n return ['@qualflare/playwright/reporter', options];\n}\n","import { test } from '@playwright/test';\n\nimport { RESERVED_MESSAGE_MEDIA_TYPE } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { CasePriority, LinkType } from '../shared/types.js';\nimport type { RuntimeMessage } from './message-types.js';\n\n/**\n * Ships one structured message from the test process to the reporter.\n *\n * Playwright runs tests in worker processes and reporters in the main\n * process, with no shared memory — the only user-reachable channel back is\n * `testInfo.attach()`. So every `qualflare.*()` call is serialized and\n * attached under a reserved content type; the reporter recognizes that exact\n * type in `onTestEnd`, replays it as a model mutation, and excludes it from\n * real attachment processing. Same trick `@qualflare/cucumberjs` plays with\n * `World.attach()`, for the same reason.\n *\n * `test.info()` throws when called outside a running test (module scope, a\n * `globalSetup`, a stray import). Warn and drop: a metadata call is never\n * worth failing somebody's suite over.\n */\nfunction send(message: RuntimeMessage): void {\n try {\n // The .catch() is not optional. `attach()` returns a promise, and an\n // unhandled rejection TERMINATES the process by default on Node >= 15 —\n // a metadata call would take the user's entire test run down with it.\n // The surrounding try/catch only covers a synchronous throw from\n // `test.info()` (called outside a running test), not this.\n void test\n .info()\n .attach(`qualflare:${message.type}`, {\n body: Buffer.from(JSON.stringify(message), 'utf8'),\n contentType: RESERVED_MESSAGE_MEDIA_TYPE,\n })\n .catch((err: unknown) => {\n logger.warn(`qualflare.${message.type}() could not be recorded: ${(err as Error).message}`);\n });\n } catch {\n logger.warn(`qualflare.${message.type}() was called outside a running test; the call was ignored.`);\n }\n}\n\n/**\n * Author-facing metadata API. Import it in a spec and annotate tests with\n * business context Playwright itself has no concept of:\n *\n * ```ts\n * import { qualflare } from '@qualflare/playwright';\n *\n * test('checks out', async ({ page }) => {\n * qualflare.label('epic', 'Billing');\n * qualflare.link('https://tracker/QF-1', { type: 'issue', name: 'QF-1' });\n * await qualflare.step('pay', async () => { ... });\n * });\n * ```\n */\nexport const qualflare = {\n /** Arbitrary name/value metadata (epic, feature, story, owner, severity). */\n label(name: string, value: string): void {\n send({ type: 'label', name, value });\n },\n\n /** A typed external reference. `type` defaults to `custom`. */\n link(url: string, opts?: { type?: LinkType; name?: string }): void {\n send({ type: 'link', url, linkType: opts?.type, name: opts?.name });\n },\n\n /** One or more free-text tags. */\n tag(...tags: string[]): void {\n send({ type: 'tag', tags });\n },\n\n /** Markdown description shown on the case. */\n description(text: string): void {\n send({ type: 'description', text });\n },\n\n /** Case priority (low | medium | high | critical). */\n priority(value: CasePriority): void {\n send({ type: 'priority', value });\n },\n\n /** A named input. Inside an open `step()` it attaches to that step;\n * outside any step it lands in the case's properties. `masked` is a\n * display hint for the UI only — the server does not redact the value. */\n parameter(name: string, value?: string, opts?: { masked?: boolean }): void {\n send({ type: 'parameter', name, value, masked: opts?.masked });\n },\n\n /** Attach in-memory content. */\n attachment(name: string, content: string, opts?: { encoding?: 'utf8' | 'base64'; mimeType?: string }): void {\n const contentBase64 = opts?.encoding === 'base64' ? content : Buffer.from(content, 'utf8').toString('base64');\n send({ type: 'attachment', name, contentBase64, mimeType: opts?.mimeType });\n },\n\n /** Attach a file from disk. */\n attachmentFromFile(name: string, path: string, opts?: { mimeType?: string }): void {\n send({ type: 'attachment_from_file', name, path, mimeType: opts?.mimeType });\n },\n\n /**\n * Records a named step around `fn`.\n *\n * Unlike the cucumber-js equivalent, this delegates to Playwright's own\n * `test.step()` as well, so the step shows up in Playwright's HTML report\n * and trace viewer in addition to Qualflare — there is no reason to make\n * users choose, and a step that exists in only one of the two is a\n * confusing thing to debug against.\n */\n async step<T>(name: string, fn: () => T | Promise<T>): Promise<T> {\n return test.step(name, async () => {\n send({ type: 'step_start', name, timestamp: Date.now() });\n try {\n const result = await fn();\n send({ type: 'step_stop', status: 'passed', timestamp: Date.now() });\n return result;\n } catch (err) {\n send({\n type: 'step_stop',\n status: 'failed',\n error: err instanceof Error ? err.message : String(err),\n timestamp: Date.now(),\n });\n throw err;\n }\n });\n },\n};\n","/**\n * Shared constants used across the reporter and the author-facing runtime\n * API.\n */\n\n/** Reserved `testInfo.attach()` content type used to smuggle structured\n * `qualflare.*()` calls (label/tag/step/etc.) from test and hook code back to\n * the reporter — the only channel Playwright gives user code back to a\n * running reporter. The reporter recognizes this exact content type and\n * replays the message as a model mutation instead of reporting it as a\n * literal attachment. */\nexport const RESERVED_MESSAGE_MEDIA_TYPE = 'application/vnd.qualflare.message+json';\n\n/** Server-side caps this client should respect defensively (see\n * `api-service/internal/core/domain/launch/launch.go`). */\nexport const MAX_SUITES_PER_LAUNCH = 2000;\nexport const MAX_CASES_PER_SUITE = 5000;\nexport const MAX_STEPS_PER_CASE = 1000;\nexport const MAX_PARAMETERS_PER_STEP = 50;\nexport const MAX_ATTACHMENTS_PER_CASE = 50;\nexport const MAX_LABELS_PER_CASE = 100;\nexport const MAX_LINKS_PER_CASE = 20;\nexport const MAX_TAGS_PER_CASE = 64;\nexport const MAX_TAG_LENGTH = 255;\n\n/** Mirrors `launch.MaxCaseAttempts`. Beyond this the server keeps the first\n * 49 attempts plus the final one and drops the middle, so sending more is\n * wasted payload rather than an error. */\nexport const MAX_ATTEMPTS_PER_CASE = 50;\n\n/** Mirrors the server's per-attempt text bounds (`launch.MaxAttempt*Runes`).\n *\n * Clamped CLIENT-side, not left to the server, because attempts are the only\n * repeated-per-case payload with no size budget of its own. Measured: one\n * retried test with a deep stack and a chatty log serializes to ~630KB\n * unclamped — most of it text the server discards on write — against a 10MB\n * request body limit that, once exceeded, loses the ENTIRE launch. Sending\n * bytes the server will throw away is pure risk. */\nexport const MAX_ATTEMPT_MESSAGE_RUNES = 8192;\nexport const MAX_ATTEMPT_TRACE_RUNES = 32768;\nexport const MAX_ATTEMPT_SNIPPET_RUNES = 4096;\nexport const MAX_ATTEMPT_OUTPUT_RUNES = 16384;\nexport const MAX_ATTEMPT_OUTPUT_LINES = 200;\n\n/** Mirrors `launch.MaxAttachmentUploadFileSize` — the server's hard cap on a\n * single `POST /api/v1/attachments/upload-url` request (video). */\nexport const MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;\n\n/** Cap on one Playwright trace zip. Matches MAX_VIDEO_UPLOAD_BYTES because both\n * end up at the same server-side limit: the attachment upload endpoint rejects\n * anything past 50MB regardless of kind. Copying a larger trace would only\n * defer the rejection to collect time, after the bytes were already written. */\nexport const MAX_TRACE_UPLOAD_BYTES = 50 * 1024 * 1024;\n\n/** Client-side SOFT cap on steps recorded per scenario attempt — well under\n * the server's 1000-per-case hard cap (`MAX_STEPS_PER_CASE`). Once hit,\n * further steps within that attempt are dropped (with a one-time warning),\n * not queued and truncated later. */\nexport const MAX_STEPS_PER_TEST_ATTEMPT = 300;\n","/**\n * A minimal logger writing to stderr. Deliberately avoids stdout, since\n * that's typically Playwright's own reporter output stream and shouldn't be\n * polluted with reporter diagnostics.\n */\n\nconst PREFIX = '[qualflare-playwright]';\n\nexport const logger = {\n debug(...args: unknown[]): void {\n console.debug(PREFIX, ...args);\n },\n info(...args: unknown[]): void {\n console.log(PREFIX, ...args);\n },\n warn(...args: unknown[]): void {\n console.warn(PREFIX, ...args);\n },\n error(...args: unknown[]): void {\n console.error(PREFIX, ...args);\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAqB;;;ACWd,IAAM,8BAA8B;AAmCpC,IAAM,yBAAyB,KAAK,OAAO;AAM3C,IAAM,yBAAyB,KAAK,OAAO;;;AC9ClD,IAAM,SAAS;AAER,IAAM,SAAS;AAAA,EACpB,SAAS,MAAuB;AAC9B,YAAQ,MAAM,QAAQ,GAAG,IAAI;AAAA,EAC/B;AAAA,EACA,QAAQ,MAAuB;AAC7B,YAAQ,IAAI,QAAQ,GAAG,IAAI;AAAA,EAC7B;AAAA,EACA,QAAQ,MAAuB;AAC7B,YAAQ,KAAK,QAAQ,GAAG,IAAI;AAAA,EAC9B;AAAA,EACA,SAAS,MAAuB;AAC9B,YAAQ,MAAM,QAAQ,GAAG,IAAI;AAAA,EAC/B;AACF;;;AFCA,SAAS,KAAK,SAA+B;AAC3C,MAAI;AAMF,SAAK,iBACF,KAAK,EACL,OAAO,aAAa,QAAQ,IAAI,IAAI;AAAA,MACnC,MAAM,OAAO,KAAK,KAAK,UAAU,OAAO,GAAG,MAAM;AAAA,MACjD,aAAa;AAAA,IACf,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,aAAO,KAAK,aAAa,QAAQ,IAAI,6BAA8B,IAAc,OAAO,EAAE;AAAA,IAC5F,CAAC;AAAA,EACL,QAAQ;AACN,WAAO,KAAK,aAAa,QAAQ,IAAI,6DAA6D;AAAA,EACpG;AACF;AAgBO,IAAM,YAAY;AAAA;AAAA,EAEvB,MAAM,MAAc,OAAqB;AACvC,SAAK,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC;AAAA,EACrC;AAAA;AAAA,EAGA,KAAK,KAAa,MAAiD;AACjE,SAAK,EAAE,MAAM,QAAQ,KAAK,UAAU,MAAM,MAAM,MAAM,MAAM,KAAK,CAAC;AAAA,EACpE;AAAA;AAAA,EAGA,OAAO,MAAsB;AAC3B,SAAK,EAAE,MAAM,OAAO,KAAK,CAAC;AAAA,EAC5B;AAAA;AAAA,EAGA,YAAY,MAAoB;AAC9B,SAAK,EAAE,MAAM,eAAe,KAAK,CAAC;AAAA,EACpC;AAAA;AAAA,EAGA,SAAS,OAA2B;AAClC,SAAK,EAAE,MAAM,YAAY,MAAM,CAAC;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,MAAc,OAAgB,MAAmC;AACzE,SAAK,EAAE,MAAM,aAAa,MAAM,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,EAC/D;AAAA;AAAA,EAGA,WAAW,MAAc,SAAiB,MAAkE;AAC1G,UAAM,gBAAgB,MAAM,aAAa,WAAW,UAAU,OAAO,KAAK,SAAS,MAAM,EAAE,SAAS,QAAQ;AAC5G,SAAK,EAAE,MAAM,cAAc,MAAM,eAAe,UAAU,MAAM,SAAS,CAAC;AAAA,EAC5E;AAAA;AAAA,EAGA,mBAAmB,MAAc,MAAc,MAAoC;AACjF,SAAK,EAAE,MAAM,wBAAwB,MAAM,MAAM,UAAU,MAAM,SAAS,CAAC;AAAA,EAC7E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,KAAQ,MAAc,IAAsC;AAChE,WAAO,iBAAK,KAAK,MAAM,YAAY;AACjC,WAAK,EAAE,MAAM,cAAc,MAAM,WAAW,KAAK,IAAI,EAAE,CAAC;AACxD,UAAI;AACF,cAAM,SAAS,MAAM,GAAG;AACxB,aAAK,EAAE,MAAM,aAAa,QAAQ,UAAU,WAAW,KAAK,IAAI,EAAE,CAAC;AACnE,eAAO;AAAA,MACT,SAAS,KAAK;AACZ,aAAK;AAAA,UACH,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,UACtD,WAAW,KAAK,IAAI;AAAA,QACtB,CAAC;AACD,cAAM;AAAA,MACR;AAAA,IACF,CAAC;AAAA,EACH;AACF;;;ADrFO,SAAS,kBAAkB,UAAsC,CAAC,GAAwB;AAC/F,SAAO,CAAC,kCAAkC,OAAO;AACnD;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReporterDescription } from '@playwright/test';
|
|
2
|
-
import { L as LinkType, C as CasePriority, Q as QualflarePlaywrightOptions } from './resolve-config-
|
|
3
|
-
export { A as Attachment, a as Case, b as CaseStatus, c as Collect, F as FrameworkCategory, d as Label, e as Link, M as Metadata, N as NanosecondDuration, P as Parameter, f as Platform, R as ResolvedReporterConfig, S as Step, g as Suite } from './resolve-config-
|
|
2
|
+
import { L as LinkType, C as CasePriority, Q as QualflarePlaywrightOptions } from './resolve-config-T2ChZzAm.cjs';
|
|
3
|
+
export { A as Attachment, a as Case, b as CaseStatus, c as Collect, F as FrameworkCategory, d as Label, e as Link, M as Metadata, N as NanosecondDuration, P as Parameter, f as Platform, R as ResolvedReporterConfig, S as Step, g as Suite } from './resolve-config-T2ChZzAm.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Author-facing metadata API. Import it in a spec and annotate tests with
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReporterDescription } from '@playwright/test';
|
|
2
|
-
import { L as LinkType, C as CasePriority, Q as QualflarePlaywrightOptions } from './resolve-config-
|
|
3
|
-
export { A as Attachment, a as Case, b as CaseStatus, c as Collect, F as FrameworkCategory, d as Label, e as Link, M as Metadata, N as NanosecondDuration, P as Parameter, f as Platform, R as ResolvedReporterConfig, S as Step, g as Suite } from './resolve-config-
|
|
2
|
+
import { L as LinkType, C as CasePriority, Q as QualflarePlaywrightOptions } from './resolve-config-T2ChZzAm.js';
|
|
3
|
+
export { A as Attachment, a as Case, b as CaseStatus, c as Collect, F as FrameworkCategory, d as Label, e as Link, M as Metadata, N as NanosecondDuration, P as Parameter, f as Platform, R as ResolvedReporterConfig, S as Step, g as Suite } from './resolve-config-T2ChZzAm.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Author-facing metadata API. Import it in a spec and annotate tests with
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { test } from "@playwright/test";
|
|
|
4
4
|
// src/shared/constants.ts
|
|
5
5
|
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
6
6
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
7
|
+
var MAX_TRACE_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
7
8
|
|
|
8
9
|
// src/shared/logger.ts
|
|
9
10
|
var PREFIX = "[qualflare-playwright]";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime/qualflare-api.ts","../src/shared/constants.ts","../src/shared/logger.ts","../src/index.ts"],"sourcesContent":["import { test } from '@playwright/test';\n\nimport { RESERVED_MESSAGE_MEDIA_TYPE } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { CasePriority, LinkType } from '../shared/types.js';\nimport type { RuntimeMessage } from './message-types.js';\n\n/**\n * Ships one structured message from the test process to the reporter.\n *\n * Playwright runs tests in worker processes and reporters in the main\n * process, with no shared memory — the only user-reachable channel back is\n * `testInfo.attach()`. So every `qualflare.*()` call is serialized and\n * attached under a reserved content type; the reporter recognizes that exact\n * type in `onTestEnd`, replays it as a model mutation, and excludes it from\n * real attachment processing. Same trick `@qualflare/cucumberjs` plays with\n * `World.attach()`, for the same reason.\n *\n * `test.info()` throws when called outside a running test (module scope, a\n * `globalSetup`, a stray import). Warn and drop: a metadata call is never\n * worth failing somebody's suite over.\n */\nfunction send(message: RuntimeMessage): void {\n try {\n // The .catch() is not optional. `attach()` returns a promise, and an\n // unhandled rejection TERMINATES the process by default on Node >= 15 —\n // a metadata call would take the user's entire test run down with it.\n // The surrounding try/catch only covers a synchronous throw from\n // `test.info()` (called outside a running test), not this.\n void test\n .info()\n .attach(`qualflare:${message.type}`, {\n body: Buffer.from(JSON.stringify(message), 'utf8'),\n contentType: RESERVED_MESSAGE_MEDIA_TYPE,\n })\n .catch((err: unknown) => {\n logger.warn(`qualflare.${message.type}() could not be recorded: ${(err as Error).message}`);\n });\n } catch {\n logger.warn(`qualflare.${message.type}() was called outside a running test; the call was ignored.`);\n }\n}\n\n/**\n * Author-facing metadata API. Import it in a spec and annotate tests with\n * business context Playwright itself has no concept of:\n *\n * ```ts\n * import { qualflare } from '@qualflare/playwright';\n *\n * test('checks out', async ({ page }) => {\n * qualflare.label('epic', 'Billing');\n * qualflare.link('https://tracker/QF-1', { type: 'issue', name: 'QF-1' });\n * await qualflare.step('pay', async () => { ... });\n * });\n * ```\n */\nexport const qualflare = {\n /** Arbitrary name/value metadata (epic, feature, story, owner, severity). */\n label(name: string, value: string): void {\n send({ type: 'label', name, value });\n },\n\n /** A typed external reference. `type` defaults to `custom`. */\n link(url: string, opts?: { type?: LinkType; name?: string }): void {\n send({ type: 'link', url, linkType: opts?.type, name: opts?.name });\n },\n\n /** One or more free-text tags. */\n tag(...tags: string[]): void {\n send({ type: 'tag', tags });\n },\n\n /** Markdown description shown on the case. */\n description(text: string): void {\n send({ type: 'description', text });\n },\n\n /** Case priority (low | medium | high | critical). */\n priority(value: CasePriority): void {\n send({ type: 'priority', value });\n },\n\n /** A named input. Inside an open `step()` it attaches to that step;\n * outside any step it lands in the case's properties. `masked` is a\n * display hint for the UI only — the server does not redact the value. */\n parameter(name: string, value?: string, opts?: { masked?: boolean }): void {\n send({ type: 'parameter', name, value, masked: opts?.masked });\n },\n\n /** Attach in-memory content. */\n attachment(name: string, content: string, opts?: { encoding?: 'utf8' | 'base64'; mimeType?: string }): void {\n const contentBase64 = opts?.encoding === 'base64' ? content : Buffer.from(content, 'utf8').toString('base64');\n send({ type: 'attachment', name, contentBase64, mimeType: opts?.mimeType });\n },\n\n /** Attach a file from disk. */\n attachmentFromFile(name: string, path: string, opts?: { mimeType?: string }): void {\n send({ type: 'attachment_from_file', name, path, mimeType: opts?.mimeType });\n },\n\n /**\n * Records a named step around `fn`.\n *\n * Unlike the cucumber-js equivalent, this delegates to Playwright's own\n * `test.step()` as well, so the step shows up in Playwright's HTML report\n * and trace viewer in addition to Qualflare — there is no reason to make\n * users choose, and a step that exists in only one of the two is a\n * confusing thing to debug against.\n */\n async step<T>(name: string, fn: () => T | Promise<T>): Promise<T> {\n return test.step(name, async () => {\n send({ type: 'step_start', name, timestamp: Date.now() });\n try {\n const result = await fn();\n send({ type: 'step_stop', status: 'passed', timestamp: Date.now() });\n return result;\n } catch (err) {\n send({\n type: 'step_stop',\n status: 'failed',\n error: err instanceof Error ? err.message : String(err),\n timestamp: Date.now(),\n });\n throw err;\n }\n });\n },\n};\n","/**\n * Shared constants used across the reporter and the author-facing runtime\n * API.\n */\n\n/** Reserved `testInfo.attach()` content type used to smuggle structured\n * `qualflare.*()` calls (label/tag/step/etc.) from test and hook code back to\n * the reporter — the only channel Playwright gives user code back to a\n * running reporter. The reporter recognizes this exact content type and\n * replays the message as a model mutation instead of reporting it as a\n * literal attachment. */\nexport const RESERVED_MESSAGE_MEDIA_TYPE = 'application/vnd.qualflare.message+json';\n\n/** Server-side caps this client should respect defensively (see\n * `api-service/internal/core/domain/launch/launch.go`). */\nexport const MAX_SUITES_PER_LAUNCH = 2000;\nexport const MAX_CASES_PER_SUITE = 5000;\nexport const MAX_STEPS_PER_CASE = 1000;\nexport const MAX_PARAMETERS_PER_STEP = 50;\nexport const MAX_ATTACHMENTS_PER_CASE = 50;\nexport const MAX_LABELS_PER_CASE = 100;\nexport const MAX_LINKS_PER_CASE = 20;\nexport const MAX_TAGS_PER_CASE = 64;\nexport const MAX_TAG_LENGTH = 255;\n\n/** Mirrors `launch.MaxAttachmentUploadFileSize` — the server's hard cap on a\n * single `POST /api/v1/attachments/upload-url` request (video). */\nexport const MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;\n\n/** Client-side SOFT cap on steps recorded per scenario attempt — well under\n * the server's 1000-per-case hard cap (`MAX_STEPS_PER_CASE`). Once hit,\n * further steps within that attempt are dropped (with a one-time warning),\n * not queued and truncated later. */\nexport const MAX_STEPS_PER_TEST_ATTEMPT = 300;\n","/**\n * A minimal logger writing to stderr. Deliberately avoids stdout, since\n * that's typically Playwright's own reporter output stream and shouldn't be\n * polluted with reporter diagnostics.\n */\n\nconst PREFIX = '[qualflare-playwright]';\n\nexport const logger = {\n debug(...args: unknown[]): void {\n console.debug(PREFIX, ...args);\n },\n info(...args: unknown[]): void {\n console.log(PREFIX, ...args);\n },\n warn(...args: unknown[]): void {\n console.warn(PREFIX, ...args);\n },\n error(...args: unknown[]): void {\n console.error(PREFIX, ...args);\n },\n};\n","import type { ReporterDescription } from '@playwright/test';\n\nimport type { QualflarePlaywrightOptions } from './config/resolve-config.js';\n\nexport { qualflare } from './runtime/qualflare-api.js';\n\nexport type { QualflarePlaywrightOptions, ResolvedReporterConfig } from './config/resolve-config.js';\n\nexport type {\n Attachment,\n Case,\n CasePriority,\n CaseStatus,\n Collect,\n FrameworkCategory,\n Label,\n Link,\n LinkType,\n Metadata,\n NanosecondDuration,\n Parameter,\n Platform,\n Step,\n Suite,\n} from './shared/types.js';\n\n/**\n * Typed helper for registering the reporter.\n *\n * Playwright types a reporter's options as `any` (`ReporterDescription` ends\n * in `[string, any]`), so writing the tuple by hand gives no autocomplete and\n * silently accepts typos. This returns the same tuple with the options\n * checked:\n *\n * ```ts\n * import { defineConfig } from '@playwright/test';\n * import { qualflareReporter } from '@qualflare/playwright';\n *\n * export default defineConfig({\n * reporter: [['list'], qualflareReporter({ environment: 'staging' })],\n * });\n * ```\n */\nexport function qualflareReporter(options: QualflarePlaywrightOptions = {}): ReporterDescription {\n return ['@qualflare/playwright/reporter', options];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/runtime/qualflare-api.ts","../src/shared/constants.ts","../src/shared/logger.ts","../src/index.ts"],"sourcesContent":["import { test } from '@playwright/test';\n\nimport { RESERVED_MESSAGE_MEDIA_TYPE } from '../shared/constants.js';\nimport { logger } from '../shared/logger.js';\nimport type { CasePriority, LinkType } from '../shared/types.js';\nimport type { RuntimeMessage } from './message-types.js';\n\n/**\n * Ships one structured message from the test process to the reporter.\n *\n * Playwright runs tests in worker processes and reporters in the main\n * process, with no shared memory — the only user-reachable channel back is\n * `testInfo.attach()`. So every `qualflare.*()` call is serialized and\n * attached under a reserved content type; the reporter recognizes that exact\n * type in `onTestEnd`, replays it as a model mutation, and excludes it from\n * real attachment processing. Same trick `@qualflare/cucumberjs` plays with\n * `World.attach()`, for the same reason.\n *\n * `test.info()` throws when called outside a running test (module scope, a\n * `globalSetup`, a stray import). Warn and drop: a metadata call is never\n * worth failing somebody's suite over.\n */\nfunction send(message: RuntimeMessage): void {\n try {\n // The .catch() is not optional. `attach()` returns a promise, and an\n // unhandled rejection TERMINATES the process by default on Node >= 15 —\n // a metadata call would take the user's entire test run down with it.\n // The surrounding try/catch only covers a synchronous throw from\n // `test.info()` (called outside a running test), not this.\n void test\n .info()\n .attach(`qualflare:${message.type}`, {\n body: Buffer.from(JSON.stringify(message), 'utf8'),\n contentType: RESERVED_MESSAGE_MEDIA_TYPE,\n })\n .catch((err: unknown) => {\n logger.warn(`qualflare.${message.type}() could not be recorded: ${(err as Error).message}`);\n });\n } catch {\n logger.warn(`qualflare.${message.type}() was called outside a running test; the call was ignored.`);\n }\n}\n\n/**\n * Author-facing metadata API. Import it in a spec and annotate tests with\n * business context Playwright itself has no concept of:\n *\n * ```ts\n * import { qualflare } from '@qualflare/playwright';\n *\n * test('checks out', async ({ page }) => {\n * qualflare.label('epic', 'Billing');\n * qualflare.link('https://tracker/QF-1', { type: 'issue', name: 'QF-1' });\n * await qualflare.step('pay', async () => { ... });\n * });\n * ```\n */\nexport const qualflare = {\n /** Arbitrary name/value metadata (epic, feature, story, owner, severity). */\n label(name: string, value: string): void {\n send({ type: 'label', name, value });\n },\n\n /** A typed external reference. `type` defaults to `custom`. */\n link(url: string, opts?: { type?: LinkType; name?: string }): void {\n send({ type: 'link', url, linkType: opts?.type, name: opts?.name });\n },\n\n /** One or more free-text tags. */\n tag(...tags: string[]): void {\n send({ type: 'tag', tags });\n },\n\n /** Markdown description shown on the case. */\n description(text: string): void {\n send({ type: 'description', text });\n },\n\n /** Case priority (low | medium | high | critical). */\n priority(value: CasePriority): void {\n send({ type: 'priority', value });\n },\n\n /** A named input. Inside an open `step()` it attaches to that step;\n * outside any step it lands in the case's properties. `masked` is a\n * display hint for the UI only — the server does not redact the value. */\n parameter(name: string, value?: string, opts?: { masked?: boolean }): void {\n send({ type: 'parameter', name, value, masked: opts?.masked });\n },\n\n /** Attach in-memory content. */\n attachment(name: string, content: string, opts?: { encoding?: 'utf8' | 'base64'; mimeType?: string }): void {\n const contentBase64 = opts?.encoding === 'base64' ? content : Buffer.from(content, 'utf8').toString('base64');\n send({ type: 'attachment', name, contentBase64, mimeType: opts?.mimeType });\n },\n\n /** Attach a file from disk. */\n attachmentFromFile(name: string, path: string, opts?: { mimeType?: string }): void {\n send({ type: 'attachment_from_file', name, path, mimeType: opts?.mimeType });\n },\n\n /**\n * Records a named step around `fn`.\n *\n * Unlike the cucumber-js equivalent, this delegates to Playwright's own\n * `test.step()` as well, so the step shows up in Playwright's HTML report\n * and trace viewer in addition to Qualflare — there is no reason to make\n * users choose, and a step that exists in only one of the two is a\n * confusing thing to debug against.\n */\n async step<T>(name: string, fn: () => T | Promise<T>): Promise<T> {\n return test.step(name, async () => {\n send({ type: 'step_start', name, timestamp: Date.now() });\n try {\n const result = await fn();\n send({ type: 'step_stop', status: 'passed', timestamp: Date.now() });\n return result;\n } catch (err) {\n send({\n type: 'step_stop',\n status: 'failed',\n error: err instanceof Error ? err.message : String(err),\n timestamp: Date.now(),\n });\n throw err;\n }\n });\n },\n};\n","/**\n * Shared constants used across the reporter and the author-facing runtime\n * API.\n */\n\n/** Reserved `testInfo.attach()` content type used to smuggle structured\n * `qualflare.*()` calls (label/tag/step/etc.) from test and hook code back to\n * the reporter — the only channel Playwright gives user code back to a\n * running reporter. The reporter recognizes this exact content type and\n * replays the message as a model mutation instead of reporting it as a\n * literal attachment. */\nexport const RESERVED_MESSAGE_MEDIA_TYPE = 'application/vnd.qualflare.message+json';\n\n/** Server-side caps this client should respect defensively (see\n * `api-service/internal/core/domain/launch/launch.go`). */\nexport const MAX_SUITES_PER_LAUNCH = 2000;\nexport const MAX_CASES_PER_SUITE = 5000;\nexport const MAX_STEPS_PER_CASE = 1000;\nexport const MAX_PARAMETERS_PER_STEP = 50;\nexport const MAX_ATTACHMENTS_PER_CASE = 50;\nexport const MAX_LABELS_PER_CASE = 100;\nexport const MAX_LINKS_PER_CASE = 20;\nexport const MAX_TAGS_PER_CASE = 64;\nexport const MAX_TAG_LENGTH = 255;\n\n/** Mirrors `launch.MaxCaseAttempts`. Beyond this the server keeps the first\n * 49 attempts plus the final one and drops the middle, so sending more is\n * wasted payload rather than an error. */\nexport const MAX_ATTEMPTS_PER_CASE = 50;\n\n/** Mirrors the server's per-attempt text bounds (`launch.MaxAttempt*Runes`).\n *\n * Clamped CLIENT-side, not left to the server, because attempts are the only\n * repeated-per-case payload with no size budget of its own. Measured: one\n * retried test with a deep stack and a chatty log serializes to ~630KB\n * unclamped — most of it text the server discards on write — against a 10MB\n * request body limit that, once exceeded, loses the ENTIRE launch. Sending\n * bytes the server will throw away is pure risk. */\nexport const MAX_ATTEMPT_MESSAGE_RUNES = 8192;\nexport const MAX_ATTEMPT_TRACE_RUNES = 32768;\nexport const MAX_ATTEMPT_SNIPPET_RUNES = 4096;\nexport const MAX_ATTEMPT_OUTPUT_RUNES = 16384;\nexport const MAX_ATTEMPT_OUTPUT_LINES = 200;\n\n/** Mirrors `launch.MaxAttachmentUploadFileSize` — the server's hard cap on a\n * single `POST /api/v1/attachments/upload-url` request (video). */\nexport const MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;\n\n/** Cap on one Playwright trace zip. Matches MAX_VIDEO_UPLOAD_BYTES because both\n * end up at the same server-side limit: the attachment upload endpoint rejects\n * anything past 50MB regardless of kind. Copying a larger trace would only\n * defer the rejection to collect time, after the bytes were already written. */\nexport const MAX_TRACE_UPLOAD_BYTES = 50 * 1024 * 1024;\n\n/** Client-side SOFT cap on steps recorded per scenario attempt — well under\n * the server's 1000-per-case hard cap (`MAX_STEPS_PER_CASE`). Once hit,\n * further steps within that attempt are dropped (with a one-time warning),\n * not queued and truncated later. */\nexport const MAX_STEPS_PER_TEST_ATTEMPT = 300;\n","/**\n * A minimal logger writing to stderr. Deliberately avoids stdout, since\n * that's typically Playwright's own reporter output stream and shouldn't be\n * polluted with reporter diagnostics.\n */\n\nconst PREFIX = '[qualflare-playwright]';\n\nexport const logger = {\n debug(...args: unknown[]): void {\n console.debug(PREFIX, ...args);\n },\n info(...args: unknown[]): void {\n console.log(PREFIX, ...args);\n },\n warn(...args: unknown[]): void {\n console.warn(PREFIX, ...args);\n },\n error(...args: unknown[]): void {\n console.error(PREFIX, ...args);\n },\n};\n","import type { ReporterDescription } from '@playwright/test';\n\nimport type { QualflarePlaywrightOptions } from './config/resolve-config.js';\n\nexport { qualflare } from './runtime/qualflare-api.js';\n\nexport type { QualflarePlaywrightOptions, ResolvedReporterConfig } from './config/resolve-config.js';\n\nexport type {\n Attachment,\n Case,\n CasePriority,\n CaseStatus,\n Collect,\n FrameworkCategory,\n Label,\n Link,\n LinkType,\n Metadata,\n NanosecondDuration,\n Parameter,\n Platform,\n Step,\n Suite,\n} from './shared/types.js';\n\n/**\n * Typed helper for registering the reporter.\n *\n * Playwright types a reporter's options as `any` (`ReporterDescription` ends\n * in `[string, any]`), so writing the tuple by hand gives no autocomplete and\n * silently accepts typos. This returns the same tuple with the options\n * checked:\n *\n * ```ts\n * import { defineConfig } from '@playwright/test';\n * import { qualflareReporter } from '@qualflare/playwright';\n *\n * export default defineConfig({\n * reporter: [['list'], qualflareReporter({ environment: 'staging' })],\n * });\n * ```\n */\nexport function qualflareReporter(options: QualflarePlaywrightOptions = {}): ReporterDescription {\n return ['@qualflare/playwright/reporter', options];\n}\n"],"mappings":";AAAA,SAAS,YAAY;;;ACWd,IAAM,8BAA8B;AAmCpC,IAAM,yBAAyB,KAAK,OAAO;AAM3C,IAAM,yBAAyB,KAAK,OAAO;;;AC9ClD,IAAM,SAAS;AAER,IAAM,SAAS;AAAA,EACpB,SAAS,MAAuB;AAC9B,YAAQ,MAAM,QAAQ,GAAG,IAAI;AAAA,EAC/B;AAAA,EACA,QAAQ,MAAuB;AAC7B,YAAQ,IAAI,QAAQ,GAAG,IAAI;AAAA,EAC7B;AAAA,EACA,QAAQ,MAAuB;AAC7B,YAAQ,KAAK,QAAQ,GAAG,IAAI;AAAA,EAC9B;AAAA,EACA,SAAS,MAAuB;AAC9B,YAAQ,MAAM,QAAQ,GAAG,IAAI;AAAA,EAC/B;AACF;;;AFCA,SAAS,KAAK,SAA+B;AAC3C,MAAI;AAMF,SAAK,KACF,KAAK,EACL,OAAO,aAAa,QAAQ,IAAI,IAAI;AAAA,MACnC,MAAM,OAAO,KAAK,KAAK,UAAU,OAAO,GAAG,MAAM;AAAA,MACjD,aAAa;AAAA,IACf,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,aAAO,KAAK,aAAa,QAAQ,IAAI,6BAA8B,IAAc,OAAO,EAAE;AAAA,IAC5F,CAAC;AAAA,EACL,QAAQ;AACN,WAAO,KAAK,aAAa,QAAQ,IAAI,6DAA6D;AAAA,EACpG;AACF;AAgBO,IAAM,YAAY;AAAA;AAAA,EAEvB,MAAM,MAAc,OAAqB;AACvC,SAAK,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC;AAAA,EACrC;AAAA;AAAA,EAGA,KAAK,KAAa,MAAiD;AACjE,SAAK,EAAE,MAAM,QAAQ,KAAK,UAAU,MAAM,MAAM,MAAM,MAAM,KAAK,CAAC;AAAA,EACpE;AAAA;AAAA,EAGA,OAAO,MAAsB;AAC3B,SAAK,EAAE,MAAM,OAAO,KAAK,CAAC;AAAA,EAC5B;AAAA;AAAA,EAGA,YAAY,MAAoB;AAC9B,SAAK,EAAE,MAAM,eAAe,KAAK,CAAC;AAAA,EACpC;AAAA;AAAA,EAGA,SAAS,OAA2B;AAClC,SAAK,EAAE,MAAM,YAAY,MAAM,CAAC;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,MAAc,OAAgB,MAAmC;AACzE,SAAK,EAAE,MAAM,aAAa,MAAM,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,EAC/D;AAAA;AAAA,EAGA,WAAW,MAAc,SAAiB,MAAkE;AAC1G,UAAM,gBAAgB,MAAM,aAAa,WAAW,UAAU,OAAO,KAAK,SAAS,MAAM,EAAE,SAAS,QAAQ;AAC5G,SAAK,EAAE,MAAM,cAAc,MAAM,eAAe,UAAU,MAAM,SAAS,CAAC;AAAA,EAC5E;AAAA;AAAA,EAGA,mBAAmB,MAAc,MAAc,MAAoC;AACjF,SAAK,EAAE,MAAM,wBAAwB,MAAM,MAAM,UAAU,MAAM,SAAS,CAAC;AAAA,EAC7E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,KAAQ,MAAc,IAAsC;AAChE,WAAO,KAAK,KAAK,MAAM,YAAY;AACjC,WAAK,EAAE,MAAM,cAAc,MAAM,WAAW,KAAK,IAAI,EAAE,CAAC;AACxD,UAAI;AACF,cAAM,SAAS,MAAM,GAAG;AACxB,aAAK,EAAE,MAAM,aAAa,QAAQ,UAAU,WAAW,KAAK,IAAI,EAAE,CAAC;AACnE,eAAO;AAAA,MACT,SAAS,KAAK;AACZ,aAAK;AAAA,UACH,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,UACtD,WAAW,KAAK,IAAI;AAAA,QACtB,CAAC;AACD,cAAM;AAAA,MACR;AAAA,IACF,CAAC;AAAA,EACH;AACF;;;AGrFO,SAAS,kBAAkB,UAAsC,CAAC,GAAwB;AAC/F,SAAO,CAAC,kCAAkC,OAAO;AACnD;","names":[]}
|
package/dist/reporter/index.cjs
CHANGED
|
@@ -51,7 +51,14 @@ var MAX_LABELS_PER_CASE = 100;
|
|
|
51
51
|
var MAX_LINKS_PER_CASE = 20;
|
|
52
52
|
var MAX_TAGS_PER_CASE = 64;
|
|
53
53
|
var MAX_TAG_LENGTH = 255;
|
|
54
|
+
var MAX_ATTEMPTS_PER_CASE = 50;
|
|
55
|
+
var MAX_ATTEMPT_MESSAGE_RUNES = 8192;
|
|
56
|
+
var MAX_ATTEMPT_TRACE_RUNES = 32768;
|
|
57
|
+
var MAX_ATTEMPT_SNIPPET_RUNES = 4096;
|
|
58
|
+
var MAX_ATTEMPT_OUTPUT_RUNES = 16384;
|
|
59
|
+
var MAX_ATTEMPT_OUTPUT_LINES = 200;
|
|
54
60
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
61
|
+
var MAX_TRACE_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
55
62
|
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
56
63
|
|
|
57
64
|
// src/config/ci-detect.ts
|
|
@@ -279,6 +286,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
279
286
|
maxAttachmentBytes: options.maxAttachmentBytes ?? envInt("QUALFLARE_MAX_ATTACHMENT_BYTES") ?? 15e5,
|
|
280
287
|
maxTotalAttachmentBytes: options.maxTotalAttachmentBytes ?? envInt("QUALFLARE_MAX_TOTAL_ATTACHMENT_BYTES") ?? 75e4,
|
|
281
288
|
maxVideoBytes: options.maxVideoBytes ?? envInt("QUALFLARE_MAX_VIDEO_BYTES") ?? MAX_VIDEO_UPLOAD_BYTES,
|
|
289
|
+
maxTraceBytes: options.maxTraceBytes ?? envInt("QUALFLARE_MAX_TRACE_BYTES") ?? MAX_TRACE_UPLOAD_BYTES,
|
|
282
290
|
debug: options.debug ?? envBool("QUALFLARE_DEBUG", "QF_DEBUG") ?? false,
|
|
283
291
|
enabled,
|
|
284
292
|
outputDir,
|
|
@@ -315,35 +323,51 @@ var VIDEO_MIME_TYPES_BY_EXTENSION = {
|
|
|
315
323
|
".webm": "video/webm",
|
|
316
324
|
".mov": "video/quicktime"
|
|
317
325
|
};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (!mimeType) {
|
|
322
|
-
logger.warn(`skipping video attachment "${filePath}": unsupported video format.`);
|
|
323
|
-
return void 0;
|
|
324
|
-
}
|
|
326
|
+
var TRACE_MIME_TYPE = "application/zip";
|
|
327
|
+
var TRACE_EXTENSION = ".zip";
|
|
328
|
+
function copyArtifact(filePath, outputDir, maxBytes, ext, capName, label) {
|
|
325
329
|
let fileSize;
|
|
326
330
|
try {
|
|
327
331
|
fileSize = fs.statSync(filePath).size;
|
|
328
332
|
} catch (err) {
|
|
329
|
-
logger.warn(`skipping
|
|
333
|
+
logger.warn(`skipping ${label} attachment "${filePath}": could not stat file: ${err.message}`);
|
|
330
334
|
return void 0;
|
|
331
335
|
}
|
|
332
|
-
if (fileSize >
|
|
336
|
+
if (fileSize > maxBytes) {
|
|
333
337
|
logger.warn(
|
|
334
|
-
`skipping
|
|
338
|
+
`skipping ${label} attachment "${filePath}": ${fileSize} bytes exceeds the configured ${capName} cap of ${maxBytes} bytes.`
|
|
335
339
|
);
|
|
336
340
|
return void 0;
|
|
337
341
|
}
|
|
338
|
-
const
|
|
342
|
+
const localPath = `${(0, import_node_crypto2.randomUUID)()}${ext}`;
|
|
339
343
|
try {
|
|
340
344
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
341
|
-
fs.copyFileSync(filePath, path.join(outputDir,
|
|
345
|
+
fs.copyFileSync(filePath, path.join(outputDir, localPath));
|
|
342
346
|
} catch (err) {
|
|
343
|
-
logger.warn(`skipping
|
|
347
|
+
logger.warn(`skipping ${label} attachment "${filePath}": could not copy file: ${err.message}`);
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
return { localPath, fileSize };
|
|
351
|
+
}
|
|
352
|
+
function copyVideoAttachment(filePath, outputDir, maxVideoBytes) {
|
|
353
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
354
|
+
const mimeType = VIDEO_MIME_TYPES_BY_EXTENSION[ext];
|
|
355
|
+
if (!mimeType) {
|
|
356
|
+
logger.warn(`skipping video attachment "${filePath}": unsupported video format.`);
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
const copied = copyArtifact(filePath, outputDir, maxVideoBytes, ext, "maxVideoBytes", "video");
|
|
360
|
+
if (!copied) {
|
|
344
361
|
return void 0;
|
|
345
362
|
}
|
|
346
|
-
return { localVideoPath, fileSize, mimeType };
|
|
363
|
+
return { localVideoPath: copied.localPath, fileSize: copied.fileSize, mimeType };
|
|
364
|
+
}
|
|
365
|
+
function copyTraceAttachment(filePath, outputDir, maxTraceBytes) {
|
|
366
|
+
const copied = copyArtifact(filePath, outputDir, maxTraceBytes, TRACE_EXTENSION, "maxTraceBytes", "trace");
|
|
367
|
+
if (!copied) {
|
|
368
|
+
return void 0;
|
|
369
|
+
}
|
|
370
|
+
return { localTracePath: copied.localPath, fileSize: copied.fileSize, mimeType: TRACE_MIME_TYPE };
|
|
347
371
|
}
|
|
348
372
|
|
|
349
373
|
// src/reporter/attachment-reader.ts
|
|
@@ -373,6 +397,7 @@ var AttachmentBudget = class {
|
|
|
373
397
|
};
|
|
374
398
|
var NAME_VIDEO = "video";
|
|
375
399
|
var NAME_TRACE = "trace";
|
|
400
|
+
var TRACE_CONTENT_TYPE = "application/zip";
|
|
376
401
|
function isVideo(a) {
|
|
377
402
|
return a.name === NAME_VIDEO || (a.contentType?.startsWith("video/") ?? false);
|
|
378
403
|
}
|
|
@@ -393,7 +418,20 @@ function resolveAttachments(result, config, budget) {
|
|
|
393
418
|
}
|
|
394
419
|
break;
|
|
395
420
|
}
|
|
396
|
-
if (a.name === NAME_TRACE || a.contentType ===
|
|
421
|
+
if (a.name === NAME_TRACE || a.contentType === TRACE_CONTENT_TYPE) {
|
|
422
|
+
if (!a.path) {
|
|
423
|
+
logger.warn(`skipping in-memory trace attachment "${a.name}": only file-backed traces are supported.`);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const copied = copyTraceAttachment(a.path, config.outputDir, config.maxTraceBytes);
|
|
427
|
+
if (copied) {
|
|
428
|
+
out.push({
|
|
429
|
+
name: a.name,
|
|
430
|
+
mimeType: copied.mimeType,
|
|
431
|
+
localTracePath: copied.localTracePath,
|
|
432
|
+
fileSize: copied.fileSize
|
|
433
|
+
});
|
|
434
|
+
}
|
|
397
435
|
continue;
|
|
398
436
|
}
|
|
399
437
|
if (isVideo(a)) {
|
|
@@ -481,6 +519,34 @@ function msToNs(ms) {
|
|
|
481
519
|
return Math.round(ms * NS_PER_MS);
|
|
482
520
|
}
|
|
483
521
|
|
|
522
|
+
// src/shared/text.ts
|
|
523
|
+
function truncateRunes(value, maxRunes) {
|
|
524
|
+
if (value.length <= maxRunes) {
|
|
525
|
+
return value;
|
|
526
|
+
}
|
|
527
|
+
const runes = Array.from(value);
|
|
528
|
+
if (runes.length <= maxRunes) {
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
return runes.slice(0, maxRunes).join("");
|
|
532
|
+
}
|
|
533
|
+
function clampOutputLines(lines, maxLines, maxRunes) {
|
|
534
|
+
const out = [];
|
|
535
|
+
let budget = maxRunes;
|
|
536
|
+
for (const line of lines.slice(0, maxLines)) {
|
|
537
|
+
const cost = Array.from(line).length + 1;
|
|
538
|
+
if (cost > budget) {
|
|
539
|
+
if (budget > 1) {
|
|
540
|
+
out.push(truncateRunes(line, budget - 1));
|
|
541
|
+
}
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
out.push(line);
|
|
545
|
+
budget -= cost;
|
|
546
|
+
}
|
|
547
|
+
return out.length > 0 ? out : void 0;
|
|
548
|
+
}
|
|
549
|
+
|
|
484
550
|
// src/reporter/step-mapper.ts
|
|
485
551
|
var CATEGORY_TEST_STEP = "test.step";
|
|
486
552
|
var CATEGORY_EXPECT = "expect";
|
|
@@ -589,6 +655,63 @@ function formatError(result) {
|
|
|
589
655
|
}
|
|
590
656
|
return parts.join("\n");
|
|
591
657
|
}
|
|
658
|
+
function outputLines(chunks) {
|
|
659
|
+
if (!chunks || chunks.length === 0) {
|
|
660
|
+
return void 0;
|
|
661
|
+
}
|
|
662
|
+
const text = chunks.map((c) => typeof c === "string" ? c : c.toString("utf8")).join("");
|
|
663
|
+
const lines = stripAnsi(text).split("\n");
|
|
664
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
665
|
+
lines.pop();
|
|
666
|
+
}
|
|
667
|
+
return clampOutputLines(lines, MAX_ATTEMPT_OUTPUT_LINES, MAX_ATTEMPT_OUTPUT_RUNES);
|
|
668
|
+
}
|
|
669
|
+
function buildAttempts(results) {
|
|
670
|
+
if (results.length < 2) {
|
|
671
|
+
return void 0;
|
|
672
|
+
}
|
|
673
|
+
let kept = results;
|
|
674
|
+
if (results.length > MAX_ATTEMPTS_PER_CASE) {
|
|
675
|
+
kept = [...results.slice(0, MAX_ATTEMPTS_PER_CASE - 1), results[results.length - 1]];
|
|
676
|
+
}
|
|
677
|
+
return kept.map((r, i) => {
|
|
678
|
+
const err = r.error;
|
|
679
|
+
const attempt = {
|
|
680
|
+
// 1-based and contiguous. Deliberately the index rather than
|
|
681
|
+
// `r.retry`: results are already ordered by attempt, and a filtered-out
|
|
682
|
+
// never-ran result would leave a hole in the retry numbering that the
|
|
683
|
+
// server reads as a truncated history.
|
|
684
|
+
attempt: i + 1,
|
|
685
|
+
status: mapStatus(r.status),
|
|
686
|
+
duration: msToNs(r.duration),
|
|
687
|
+
startedAt: r.startTime.toISOString()
|
|
688
|
+
};
|
|
689
|
+
if (err) {
|
|
690
|
+
const message = err.message ?? err.value;
|
|
691
|
+
if (message) {
|
|
692
|
+
attempt.message = truncateRunes(stripAnsi(message), MAX_ATTEMPT_MESSAGE_RUNES);
|
|
693
|
+
}
|
|
694
|
+
if (err.stack) {
|
|
695
|
+
attempt.trace = truncateRunes(stripAnsi(err.stack), MAX_ATTEMPT_TRACE_RUNES);
|
|
696
|
+
}
|
|
697
|
+
if (err.snippet) {
|
|
698
|
+
attempt.snippet = truncateRunes(stripAnsi(err.snippet), MAX_ATTEMPT_SNIPPET_RUNES);
|
|
699
|
+
}
|
|
700
|
+
if (typeof err.location?.line === "number") {
|
|
701
|
+
attempt.line = err.location.line;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const stdout = outputLines(r.stdout);
|
|
705
|
+
if (stdout) {
|
|
706
|
+
attempt.stdout = stdout;
|
|
707
|
+
}
|
|
708
|
+
const stderr = outputLines(r.stderr);
|
|
709
|
+
if (stderr) {
|
|
710
|
+
attempt.stderr = stderr;
|
|
711
|
+
}
|
|
712
|
+
return attempt;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
592
715
|
function replayMetadata(result, config, budget) {
|
|
593
716
|
const meta = {
|
|
594
717
|
labels: [],
|
|
@@ -711,6 +834,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
711
834
|
const nativeTags = test.tags ?? [];
|
|
712
835
|
const tags = capTags([...nativeTags, ...meta.tags]);
|
|
713
836
|
const error = formatError(final);
|
|
837
|
+
const attempts = buildAttempts(results);
|
|
714
838
|
return {
|
|
715
839
|
id: test.id,
|
|
716
840
|
name: test.title,
|
|
@@ -719,6 +843,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
719
843
|
duration: msToNs(final.duration),
|
|
720
844
|
retryCount: results.length - 1,
|
|
721
845
|
isFlaky: outcome === "flaky",
|
|
846
|
+
...attempts ? { attempts } : {},
|
|
722
847
|
...error ? { error } : {},
|
|
723
848
|
...meta.priority ? { priority: meta.priority } : {},
|
|
724
849
|
...meta.description ? { description: meta.description } : {},
|
|
@@ -736,7 +861,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
736
861
|
var os = __toESM(require("os"), 1);
|
|
737
862
|
|
|
738
863
|
// src/config/version.ts
|
|
739
|
-
var PACKAGE_VERSION = "0.
|
|
864
|
+
var PACKAGE_VERSION = "0.4.0";
|
|
740
865
|
|
|
741
866
|
// src/reporter/collect-builder.ts
|
|
742
867
|
function resolveOs(config) {
|
|
@@ -927,7 +1052,8 @@ var QualflareReporter = class {
|
|
|
927
1052
|
return path3.isAbsolute(outputDir) ? outputDir : path3.resolve(this.options.configDir ?? this.rootDir, outputDir);
|
|
928
1053
|
}
|
|
929
1054
|
/** Drops everything an earlier, now-superseded attempt produced: deletes the
|
|
930
|
-
* video copied into outputDir and refunds its bytes to the run
|
|
1055
|
+
* video or trace copied into outputDir and refunds its bytes to the run
|
|
1056
|
+
* budget. */
|
|
931
1057
|
discardSupersededAttempt(testId, retry, outputDir) {
|
|
932
1058
|
const previous = this.latestAttemptByTest.get(testId);
|
|
933
1059
|
if (previous === void 0 || previous >= retry) {
|
|
@@ -935,9 +1061,10 @@ var QualflareReporter = class {
|
|
|
935
1061
|
}
|
|
936
1062
|
const key = `${testId}:${previous}`;
|
|
937
1063
|
for (const attachment of this.attachmentsByResult.get(key) ?? []) {
|
|
938
|
-
|
|
1064
|
+
const orphan = attachment.localVideoPath ?? attachment.localTracePath;
|
|
1065
|
+
if (orphan) {
|
|
939
1066
|
try {
|
|
940
|
-
fs3.rmSync(path3.join(this.resolveOutputDir(outputDir),
|
|
1067
|
+
fs3.rmSync(path3.join(this.resolveOutputDir(outputDir), orphan), { force: true });
|
|
941
1068
|
} catch {
|
|
942
1069
|
}
|
|
943
1070
|
}
|