@qualflare/playwright 0.1.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/dist/index.js ADDED
@@ -0,0 +1,111 @@
1
+ // src/runtime/qualflare-api.ts
2
+ import { test } from "@playwright/test";
3
+
4
+ // src/shared/constants.ts
5
+ var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
6
+ var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
7
+
8
+ // src/shared/logger.ts
9
+ var PREFIX = "[qualflare-playwright]";
10
+ var logger = {
11
+ debug(...args) {
12
+ console.debug(PREFIX, ...args);
13
+ },
14
+ info(...args) {
15
+ console.log(PREFIX, ...args);
16
+ },
17
+ warn(...args) {
18
+ console.warn(PREFIX, ...args);
19
+ },
20
+ error(...args) {
21
+ console.error(PREFIX, ...args);
22
+ }
23
+ };
24
+
25
+ // src/runtime/qualflare-api.ts
26
+ function send(message) {
27
+ try {
28
+ void test.info().attach(`qualflare:${message.type}`, {
29
+ body: Buffer.from(JSON.stringify(message), "utf8"),
30
+ contentType: RESERVED_MESSAGE_MEDIA_TYPE
31
+ }).catch((err) => {
32
+ logger.warn(`qualflare.${message.type}() could not be recorded: ${err.message}`);
33
+ });
34
+ } catch {
35
+ logger.warn(`qualflare.${message.type}() was called outside a running test; the call was ignored.`);
36
+ }
37
+ }
38
+ var qualflare = {
39
+ /** Arbitrary name/value metadata (epic, feature, story, owner, severity). */
40
+ label(name, value) {
41
+ send({ type: "label", name, value });
42
+ },
43
+ /** A typed external reference. `type` defaults to `custom`. */
44
+ link(url, opts) {
45
+ send({ type: "link", url, linkType: opts?.type, name: opts?.name });
46
+ },
47
+ /** One or more free-text tags. */
48
+ tag(...tags) {
49
+ send({ type: "tag", tags });
50
+ },
51
+ /** Markdown description shown on the case. */
52
+ description(text) {
53
+ send({ type: "description", text });
54
+ },
55
+ /** Case priority (low | medium | high | critical). */
56
+ priority(value) {
57
+ send({ type: "priority", value });
58
+ },
59
+ /** A named input. Inside an open `step()` it attaches to that step;
60
+ * outside any step it lands in the case's properties. `masked` is a
61
+ * display hint for the UI only — the server does not redact the value. */
62
+ parameter(name, value, opts) {
63
+ send({ type: "parameter", name, value, masked: opts?.masked });
64
+ },
65
+ /** Attach in-memory content. */
66
+ attachment(name, content, opts) {
67
+ const contentBase64 = opts?.encoding === "base64" ? content : Buffer.from(content, "utf8").toString("base64");
68
+ send({ type: "attachment", name, contentBase64, mimeType: opts?.mimeType });
69
+ },
70
+ /** Attach a file from disk. */
71
+ attachmentFromFile(name, path, opts) {
72
+ send({ type: "attachment_from_file", name, path, mimeType: opts?.mimeType });
73
+ },
74
+ /**
75
+ * Records a named step around `fn`.
76
+ *
77
+ * Unlike the cucumber-js equivalent, this delegates to Playwright's own
78
+ * `test.step()` as well, so the step shows up in Playwright's HTML report
79
+ * and trace viewer in addition to Qualflare — there is no reason to make
80
+ * users choose, and a step that exists in only one of the two is a
81
+ * confusing thing to debug against.
82
+ */
83
+ async step(name, fn) {
84
+ return test.step(name, async () => {
85
+ send({ type: "step_start", name, timestamp: Date.now() });
86
+ try {
87
+ const result = await fn();
88
+ send({ type: "step_stop", status: "passed", timestamp: Date.now() });
89
+ return result;
90
+ } catch (err) {
91
+ send({
92
+ type: "step_stop",
93
+ status: "failed",
94
+ error: err instanceof Error ? err.message : String(err),
95
+ timestamp: Date.now()
96
+ });
97
+ throw err;
98
+ }
99
+ });
100
+ }
101
+ };
102
+
103
+ // src/index.ts
104
+ function qualflareReporter(options = {}) {
105
+ return ["@qualflare/playwright/reporter", options];
106
+ }
107
+ export {
108
+ qualflare,
109
+ qualflareReporter
110
+ };
111
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}