@qualflare/playwright 0.1.0 → 0.3.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 +9 -3
- 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.map +1 -1
- package/dist/reporter/index.cjs +114 -6
- package/dist/reporter/index.cjs.map +1 -1
- package/dist/reporter/index.d.cts +2 -2
- package/dist/reporter/index.d.ts +2 -2
- package/dist/reporter/index.js +114 -6
- package/dist/reporter/index.js.map +1 -1
- package/dist/{resolve-config-CSjXFl7v.d.cts → resolve-config-N04M1Avn.d.cts} +67 -0
- package/dist/{resolve-config-CSjXFl7v.d.ts → resolve-config-N04M1Avn.d.ts} +67 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,12 @@ Every option has an environment-variable override, and everything has a sensible
|
|
|
105
105
|
[`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md). There is no `token` option: this reporter makes
|
|
106
106
|
no requests, so it has no credential.
|
|
107
107
|
|
|
108
|
+
One option is worth calling out because it fails late: `environment` is matched against the
|
|
109
|
+
environment's **uid (slug)**, not its display name, so **Staging** in the UI is `staging` here. A
|
|
110
|
+
wrong value cannot fail at test time — the reporter makes no requests — so the run succeeds and
|
|
111
|
+
`collect` 404s afterwards. See
|
|
112
|
+
[the note in the configuration docs](./docs/CONFIGURATION.md#environment-is-matched-by-uid-not-display-name).
|
|
113
|
+
|
|
108
114
|
## Known limitations
|
|
109
115
|
|
|
110
116
|
- **Traces are not uploaded.** Playwright traces are `application/zip`, which Qualflare's attachment
|
|
@@ -113,9 +119,9 @@ no requests, so it has no credential.
|
|
|
113
119
|
- **`pw:api` and `fixture` steps are filtered out by default** (`includeApiSteps`) — a single
|
|
114
120
|
browser test emits hundreds, which buries the steps you actually wrote. A *failed* one is always
|
|
115
121
|
kept.
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
- **A stale `outputDir` is refused, not merged** — each report carries a `runId`, and `qf collect`
|
|
123
|
+
errors rather than merging files from two different runs. Needs `@qualflare/cli` v0.1.19+; older
|
|
124
|
+
CLIs merge as before.
|
|
119
125
|
- **`merge-reports` mode is not supported** in v0.1.0 — use the `outputDir` flow above rather than
|
|
120
126
|
Playwright's `blob` reporter.
|
|
121
127
|
|
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/** 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":[]}
|
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-N04M1Avn.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-N04M1Avn.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-N04M1Avn.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-N04M1Avn.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Author-facing metadata API. Import it in a spec and annotate tests with
|
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/** 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":[]}
|
package/dist/reporter/index.cjs
CHANGED
|
@@ -35,10 +35,13 @@ __export(reporter_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(reporter_exports);
|
|
36
36
|
|
|
37
37
|
// src/reporter/reporter.ts
|
|
38
|
-
var
|
|
38
|
+
var import_node_crypto3 = require("crypto");
|
|
39
39
|
var fs3 = __toESM(require("fs"), 1);
|
|
40
40
|
var path3 = __toESM(require("path"), 1);
|
|
41
41
|
|
|
42
|
+
// src/config/resolve-config.ts
|
|
43
|
+
var import_node_crypto = require("crypto");
|
|
44
|
+
|
|
42
45
|
// src/shared/constants.ts
|
|
43
46
|
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
44
47
|
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
@@ -48,6 +51,12 @@ var MAX_LABELS_PER_CASE = 100;
|
|
|
48
51
|
var MAX_LINKS_PER_CASE = 20;
|
|
49
52
|
var MAX_TAGS_PER_CASE = 64;
|
|
50
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;
|
|
51
60
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
52
61
|
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
53
62
|
|
|
@@ -68,6 +77,7 @@ var PROVIDERS = [
|
|
|
68
77
|
detect: (env) => env.GITHUB_ACTIONS === "true",
|
|
69
78
|
providerName: "GitHub Actions",
|
|
70
79
|
buildNumber: (env) => nonEmpty(env.GITHUB_RUN_NUMBER),
|
|
80
|
+
runId: (env) => nonEmpty(env.GITHUB_RUN_ID),
|
|
71
81
|
runUrl: (env) => env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : void 0,
|
|
72
82
|
prNumber: (env) => {
|
|
73
83
|
const match = /^refs\/pull\/(\d+)\/merge$/.exec(env.GITHUB_REF ?? "");
|
|
@@ -78,6 +88,7 @@ var PROVIDERS = [
|
|
|
78
88
|
detect: (env) => env.GITLAB_CI === "true",
|
|
79
89
|
providerName: "GitLab CI",
|
|
80
90
|
buildNumber: (env) => nonEmpty(env.CI_PIPELINE_IID),
|
|
91
|
+
runId: (env) => nonEmpty(env.CI_PIPELINE_ID),
|
|
81
92
|
runUrl: (env) => nonEmpty(env.CI_PIPELINE_URL),
|
|
82
93
|
prNumber: (env) => parsePositiveInt(env.CI_MERGE_REQUEST_IID)
|
|
83
94
|
},
|
|
@@ -85,6 +96,7 @@ var PROVIDERS = [
|
|
|
85
96
|
detect: (env) => env.CIRCLECI === "true",
|
|
86
97
|
providerName: "CircleCI",
|
|
87
98
|
buildNumber: (env) => nonEmpty(env.CIRCLE_BUILD_NUM),
|
|
99
|
+
runId: (env) => nonEmpty(env.CIRCLE_WORKFLOW_ID ?? env.CIRCLE_BUILD_NUM),
|
|
88
100
|
runUrl: (env) => nonEmpty(env.CIRCLE_BUILD_URL),
|
|
89
101
|
prNumber: (env) => parsePositiveInt(env.CIRCLE_PR_NUMBER)
|
|
90
102
|
},
|
|
@@ -92,6 +104,7 @@ var PROVIDERS = [
|
|
|
92
104
|
detect: (env) => env.BUILDKITE === "true",
|
|
93
105
|
providerName: "Buildkite",
|
|
94
106
|
buildNumber: (env) => nonEmpty(env.BUILDKITE_BUILD_NUMBER),
|
|
107
|
+
runId: (env) => nonEmpty(env.BUILDKITE_BUILD_ID),
|
|
95
108
|
runUrl: (env) => nonEmpty(env.BUILDKITE_BUILD_URL),
|
|
96
109
|
prNumber: (env) => {
|
|
97
110
|
const raw = env.BUILDKITE_PULL_REQUEST;
|
|
@@ -107,6 +120,7 @@ var PROVIDERS = [
|
|
|
107
120
|
detect: (env) => Boolean(env.JENKINS_URL),
|
|
108
121
|
providerName: "Jenkins",
|
|
109
122
|
buildNumber: (env) => nonEmpty(env.BUILD_NUMBER),
|
|
123
|
+
runId: (env) => nonEmpty(env.BUILD_TAG ?? env.BUILD_NUMBER),
|
|
110
124
|
runUrl: (env) => nonEmpty(env.BUILD_URL)
|
|
111
125
|
// Jenkins has no standardized PR-number env var across its many PR
|
|
112
126
|
// plugins (Multibranch, GitHub Branch Source, etc.) — deliberately
|
|
@@ -116,6 +130,7 @@ var PROVIDERS = [
|
|
|
116
130
|
detect: (env) => env.TF_BUILD === "True" || env.TF_BUILD === "true",
|
|
117
131
|
providerName: "Azure Pipelines",
|
|
118
132
|
buildNumber: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
133
|
+
runId: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
119
134
|
runUrl: (env) => {
|
|
120
135
|
const collectionUri = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
121
136
|
const project = env.SYSTEM_TEAMPROJECT;
|
|
@@ -131,6 +146,7 @@ var PROVIDERS = [
|
|
|
131
146
|
detect: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),
|
|
132
147
|
providerName: "Bitbucket Pipelines",
|
|
133
148
|
buildNumber: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
149
|
+
runId: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
134
150
|
runUrl: (env) => {
|
|
135
151
|
const origin = env.BITBUCKET_GIT_HTTP_ORIGIN;
|
|
136
152
|
if (!origin) {
|
|
@@ -152,6 +168,8 @@ function detectCi(env = process.env) {
|
|
|
152
168
|
if (runUrl !== void 0) result.ciRunUrl = runUrl;
|
|
153
169
|
const prNumber = provider.prNumber?.(env);
|
|
154
170
|
if (prNumber !== void 0) result.ciPrNumber = prNumber;
|
|
171
|
+
const runId = provider.runId?.(env);
|
|
172
|
+
if (runId !== void 0) result.ciRunId = runId;
|
|
155
173
|
return result;
|
|
156
174
|
}
|
|
157
175
|
if (ciInfo.name) {
|
|
@@ -241,6 +259,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
241
259
|
const ciBuildNumber = options.ciBuildNumber ?? detectedCi.ciBuildNumber;
|
|
242
260
|
const ciRunUrl = options.ciRunUrl ?? detectedCi.ciRunUrl;
|
|
243
261
|
const ciPrNumber = options.ciPrNumber ?? detectedCi.ciPrNumber;
|
|
262
|
+
const runId = options.runId ?? firstEnv2("QUALFLARE_RUN_ID") ?? detectedCi.ciRunId ?? (0, import_node_crypto.randomUUID)();
|
|
244
263
|
return {
|
|
245
264
|
// `||` (truthy check), not `??`, for these three REQUIRED-non-empty wire
|
|
246
265
|
// fields — an explicit `''` option must not silently win over the
|
|
@@ -260,6 +279,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
260
279
|
ciBuildNumber,
|
|
261
280
|
ciRunUrl,
|
|
262
281
|
ciPrNumber,
|
|
282
|
+
runId,
|
|
263
283
|
attachScreenshots: options.attachScreenshots ?? envBool("QUALFLARE_ATTACH_SCREENSHOTS") ?? true,
|
|
264
284
|
includeApiSteps: options.includeApiSteps ?? envBool("QUALFLARE_INCLUDE_API_STEPS") ?? false,
|
|
265
285
|
maxAttachmentBytes: options.maxAttachmentBytes ?? envInt("QUALFLARE_MAX_ATTACHMENT_BYTES") ?? 15e5,
|
|
@@ -295,7 +315,7 @@ var fs2 = __toESM(require("fs"), 1);
|
|
|
295
315
|
// src/reporter/video-writer.ts
|
|
296
316
|
var fs = __toESM(require("fs"), 1);
|
|
297
317
|
var path = __toESM(require("path"), 1);
|
|
298
|
-
var
|
|
318
|
+
var import_node_crypto2 = require("crypto");
|
|
299
319
|
var VIDEO_MIME_TYPES_BY_EXTENSION = {
|
|
300
320
|
".mp4": "video/mp4",
|
|
301
321
|
".webm": "video/webm",
|
|
@@ -321,7 +341,7 @@ function copyVideoAttachment(filePath, outputDir, maxVideoBytes) {
|
|
|
321
341
|
);
|
|
322
342
|
return void 0;
|
|
323
343
|
}
|
|
324
|
-
const localVideoPath = `${(0,
|
|
344
|
+
const localVideoPath = `${(0, import_node_crypto2.randomUUID)()}${ext}`;
|
|
325
345
|
try {
|
|
326
346
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
327
347
|
fs.copyFileSync(filePath, path.join(outputDir, localVideoPath));
|
|
@@ -467,6 +487,34 @@ function msToNs(ms) {
|
|
|
467
487
|
return Math.round(ms * NS_PER_MS);
|
|
468
488
|
}
|
|
469
489
|
|
|
490
|
+
// src/shared/text.ts
|
|
491
|
+
function truncateRunes(value, maxRunes) {
|
|
492
|
+
if (value.length <= maxRunes) {
|
|
493
|
+
return value;
|
|
494
|
+
}
|
|
495
|
+
const runes = Array.from(value);
|
|
496
|
+
if (runes.length <= maxRunes) {
|
|
497
|
+
return value;
|
|
498
|
+
}
|
|
499
|
+
return runes.slice(0, maxRunes).join("");
|
|
500
|
+
}
|
|
501
|
+
function clampOutputLines(lines, maxLines, maxRunes) {
|
|
502
|
+
const out = [];
|
|
503
|
+
let budget = maxRunes;
|
|
504
|
+
for (const line of lines.slice(0, maxLines)) {
|
|
505
|
+
const cost = Array.from(line).length + 1;
|
|
506
|
+
if (cost > budget) {
|
|
507
|
+
if (budget > 1) {
|
|
508
|
+
out.push(truncateRunes(line, budget - 1));
|
|
509
|
+
}
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
out.push(line);
|
|
513
|
+
budget -= cost;
|
|
514
|
+
}
|
|
515
|
+
return out.length > 0 ? out : void 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
470
518
|
// src/reporter/step-mapper.ts
|
|
471
519
|
var CATEGORY_TEST_STEP = "test.step";
|
|
472
520
|
var CATEGORY_EXPECT = "expect";
|
|
@@ -575,6 +623,63 @@ function formatError(result) {
|
|
|
575
623
|
}
|
|
576
624
|
return parts.join("\n");
|
|
577
625
|
}
|
|
626
|
+
function outputLines(chunks) {
|
|
627
|
+
if (!chunks || chunks.length === 0) {
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
630
|
+
const text = chunks.map((c) => typeof c === "string" ? c : c.toString("utf8")).join("");
|
|
631
|
+
const lines = stripAnsi(text).split("\n");
|
|
632
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
633
|
+
lines.pop();
|
|
634
|
+
}
|
|
635
|
+
return clampOutputLines(lines, MAX_ATTEMPT_OUTPUT_LINES, MAX_ATTEMPT_OUTPUT_RUNES);
|
|
636
|
+
}
|
|
637
|
+
function buildAttempts(results) {
|
|
638
|
+
if (results.length < 2) {
|
|
639
|
+
return void 0;
|
|
640
|
+
}
|
|
641
|
+
let kept = results;
|
|
642
|
+
if (results.length > MAX_ATTEMPTS_PER_CASE) {
|
|
643
|
+
kept = [...results.slice(0, MAX_ATTEMPTS_PER_CASE - 1), results[results.length - 1]];
|
|
644
|
+
}
|
|
645
|
+
return kept.map((r, i) => {
|
|
646
|
+
const err = r.error;
|
|
647
|
+
const attempt = {
|
|
648
|
+
// 1-based and contiguous. Deliberately the index rather than
|
|
649
|
+
// `r.retry`: results are already ordered by attempt, and a filtered-out
|
|
650
|
+
// never-ran result would leave a hole in the retry numbering that the
|
|
651
|
+
// server reads as a truncated history.
|
|
652
|
+
attempt: i + 1,
|
|
653
|
+
status: mapStatus(r.status),
|
|
654
|
+
duration: msToNs(r.duration),
|
|
655
|
+
startedAt: r.startTime.toISOString()
|
|
656
|
+
};
|
|
657
|
+
if (err) {
|
|
658
|
+
const message = err.message ?? err.value;
|
|
659
|
+
if (message) {
|
|
660
|
+
attempt.message = truncateRunes(stripAnsi(message), MAX_ATTEMPT_MESSAGE_RUNES);
|
|
661
|
+
}
|
|
662
|
+
if (err.stack) {
|
|
663
|
+
attempt.trace = truncateRunes(stripAnsi(err.stack), MAX_ATTEMPT_TRACE_RUNES);
|
|
664
|
+
}
|
|
665
|
+
if (err.snippet) {
|
|
666
|
+
attempt.snippet = truncateRunes(stripAnsi(err.snippet), MAX_ATTEMPT_SNIPPET_RUNES);
|
|
667
|
+
}
|
|
668
|
+
if (typeof err.location?.line === "number") {
|
|
669
|
+
attempt.line = err.location.line;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const stdout = outputLines(r.stdout);
|
|
673
|
+
if (stdout) {
|
|
674
|
+
attempt.stdout = stdout;
|
|
675
|
+
}
|
|
676
|
+
const stderr = outputLines(r.stderr);
|
|
677
|
+
if (stderr) {
|
|
678
|
+
attempt.stderr = stderr;
|
|
679
|
+
}
|
|
680
|
+
return attempt;
|
|
681
|
+
});
|
|
682
|
+
}
|
|
578
683
|
function replayMetadata(result, config, budget) {
|
|
579
684
|
const meta = {
|
|
580
685
|
labels: [],
|
|
@@ -697,6 +802,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
697
802
|
const nativeTags = test.tags ?? [];
|
|
698
803
|
const tags = capTags([...nativeTags, ...meta.tags]);
|
|
699
804
|
const error = formatError(final);
|
|
805
|
+
const attempts = buildAttempts(results);
|
|
700
806
|
return {
|
|
701
807
|
id: test.id,
|
|
702
808
|
name: test.title,
|
|
@@ -705,6 +811,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
705
811
|
duration: msToNs(final.duration),
|
|
706
812
|
retryCount: results.length - 1,
|
|
707
813
|
isFlaky: outcome === "flaky",
|
|
814
|
+
...attempts ? { attempts } : {},
|
|
708
815
|
...error ? { error } : {},
|
|
709
816
|
...meta.priority ? { priority: meta.priority } : {},
|
|
710
817
|
...meta.description ? { description: meta.description } : {},
|
|
@@ -722,7 +829,7 @@ function buildCase(test, config, attachmentsByResult, budget) {
|
|
|
722
829
|
var os = __toESM(require("os"), 1);
|
|
723
830
|
|
|
724
831
|
// src/config/version.ts
|
|
725
|
-
var PACKAGE_VERSION = "0.
|
|
832
|
+
var PACKAGE_VERSION = "0.3.0";
|
|
726
833
|
|
|
727
834
|
// src/reporter/collect-builder.ts
|
|
728
835
|
function resolveOs(config) {
|
|
@@ -751,7 +858,8 @@ function buildCollectPayload(suites, config, browsers = []) {
|
|
|
751
858
|
metadata: {
|
|
752
859
|
version: PACKAGE_VERSION,
|
|
753
860
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
754
|
-
cliName: "qualflare-playwright"
|
|
861
|
+
cliName: "qualflare-playwright",
|
|
862
|
+
runId: config.runId
|
|
755
863
|
},
|
|
756
864
|
properties: config.properties,
|
|
757
865
|
suites,
|
|
@@ -901,7 +1009,7 @@ var QualflareReporter = class {
|
|
|
901
1009
|
}
|
|
902
1010
|
const outputDir = this.resolveOutputDir(config.outputDir);
|
|
903
1011
|
fs3.mkdirSync(outputDir, { recursive: true });
|
|
904
|
-
const outputPath = path3.join(outputDir, `${(0,
|
|
1012
|
+
const outputPath = path3.join(outputDir, `${(0, import_node_crypto3.randomUUID)()}.json`);
|
|
905
1013
|
fs3.writeFileSync(outputPath, JSON.stringify(collect));
|
|
906
1014
|
logger.info(`wrote Collect payload to ${outputPath} \u2014 run \`qualflare-cli collect ${outputDir}\` to upload it.`);
|
|
907
1015
|
}
|