proxymail-qa 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,6 +52,24 @@ test('sign-up sends a working code', async ({ page, inbox }) => {
52
52
  });
53
53
  ```
54
54
 
55
+ In a run (the GitHub Action, or `startRun` in `globalSetup`), the report can say
56
+ which test each email came from. Use the `qa` fixture from
57
+ `proxymail-qa/playwright`, a client that names its test in every call, and add
58
+ the reporter, which records failed and flaky tests and the totals:
59
+
60
+ ```ts
61
+ // playwright.config.ts
62
+ reporter: [['list'], ['proxymail-qa/playwright/reporter']],
63
+
64
+ // signup.spec.ts
65
+ import { test, expect } from 'proxymail-qa/playwright';
66
+
67
+ test('sign-up sends one code', async ({ page, qa }) => {
68
+ const { address } = await qa.createAddress();
69
+ // …
70
+ });
71
+ ```
72
+
55
73
  ## Cypress
56
74
 
57
75
  Register tasks in `cypress.config.ts`, since Cypress runs them in Node:
package/dist/index.d.ts CHANGED
@@ -3,6 +3,19 @@ export interface ClientOptions {
3
3
  baseUrl?: string;
4
4
  fetch?: typeof fetch;
5
5
  run?: string | null;
6
+ test?: string | null;
7
+ }
8
+ export interface TestResultInput {
9
+ name: string;
10
+ status: 'passed' | 'failed' | 'flaky' | 'skipped';
11
+ durationMs?: number;
12
+ error?: string;
13
+ }
14
+ export interface TestTotals {
15
+ passed: number;
16
+ failed: number;
17
+ skipped?: number;
18
+ flaky?: number;
6
19
  }
7
20
  export interface Persona {
8
21
  first_name: string | null;
@@ -266,7 +279,9 @@ export declare class ProxyMailQA {
266
279
  private readonly baseUrl;
267
280
  private readonly fetcher;
268
281
  run: string | null;
282
+ readonly test: string | null;
269
283
  constructor(options?: ClientOptions);
284
+ forTest(name: string): ProxyMailQA;
270
285
  private request;
271
286
  project(): Promise<Record<string, unknown>>;
272
287
  createAddress(input: CreateAddressInput & {
@@ -329,6 +344,8 @@ export declare class ProxyMailQA {
329
344
  getRun(id?: string): Promise<Run>;
330
345
  runReport(id?: string): Promise<string>;
331
346
  listRuns(limit?: number): Promise<Run[]>;
347
+ recordTest(result: TestResultInput, run?: string): Promise<void>;
348
+ recordTestTotals(totals: TestTotals, run?: string): Promise<void>;
332
349
  addRunNote(text: string, options?: {
333
350
  failed?: boolean;
334
351
  address?: string;
package/dist/index.js CHANGED
@@ -42,15 +42,25 @@ export class ProxyMailQA {
42
42
  fetcher;
43
43
  // The run this client's calls add to, if any.
44
44
  run;
45
+ // The test they belong to, if any.
46
+ test;
45
47
  constructor(options = {}) {
46
48
  const env = globalThis.process?.env;
47
49
  const apiKey = options.apiKey ?? env?.PROXYMAIL_QA_API_KEY;
48
50
  if (!apiKey)
49
51
  throw new Error('Pass apiKey or set PROXYMAIL_QA_API_KEY. Create a key in Project settings → API keys.');
50
52
  this.apiKey = apiKey;
51
- this.baseUrl = (options.baseUrl ?? 'https://api.proxymail.qa').replace(/\/+$/, '');
53
+ // PROXYMAIL_QA_API_URL, as the Python helper reads it, may end in /v1.
54
+ this.baseUrl = (options.baseUrl ?? (env?.PROXYMAIL_QA_API_URL?.trim().replace(/\/v1\/?$/, '') || 'https://api.proxymail.qa')).replace(/\/+$/, '');
52
55
  this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
53
56
  this.run = options.run === undefined ? env?.PROXYMAIL_QA_RUN?.trim() || null : options.run;
57
+ this.test = options.test?.trim() || null;
58
+ }
59
+ // A client for one test: the same key and run, and every call says which
60
+ // test it belongs to, so the run's report shows which test each email came
61
+ // from. proxymail-qa/playwright's qa fixture does this for you.
62
+ forTest(name) {
63
+ return new ProxyMailQA({ apiKey: this.apiKey, baseUrl: this.baseUrl, fetch: this.fetcher, run: this.run, test: name });
54
64
  }
55
65
  // retry: this call is asked again if nothing arrives, so the run only
56
66
  // records the last try's "nothing arrived".
@@ -61,6 +71,8 @@ export class ProxyMailQA {
61
71
  Authorization: `Bearer ${this.apiKey}`,
62
72
  ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
63
73
  ...(this.run ? { 'Proxy-Mail-QA-Run': this.run, ...(retry ? { 'Proxy-Mail-QA-Run-Retry': 'true' } : {}) } : {}),
74
+ // Percent-encoded: a test's name can hold characters headers can't.
75
+ ...(this.run && this.test ? { 'Proxy-Mail-QA-Test': encodeURIComponent(this.test) } : {}),
64
76
  },
65
77
  body: body === undefined ? undefined : JSON.stringify(body),
66
78
  });
@@ -421,6 +433,23 @@ export class ProxyMailQA {
421
433
  }
422
434
  // A line of your own in the run, such as a test that failed for a reason
423
435
  // of its own. failed: true fails the run.
436
+ // A test's result in the run (a failed one fails it), from a test runner's
437
+ // reporter: proxymail-qa/playwright/reporter sends failed and flaky tests,
438
+ // then the totals.
439
+ recordTest(result, run) {
440
+ const id = run ?? this.requireRun();
441
+ return this.request('POST', `/runs/${encodeURIComponent(id)}/tests`, {
442
+ name: result.name,
443
+ status: result.status,
444
+ duration_ms: result.durationMs,
445
+ error: result.error,
446
+ });
447
+ }
448
+ // How the suite went, for the top of the run's report.
449
+ recordTestTotals(totals, run) {
450
+ const id = run ?? this.requireRun();
451
+ return this.request('POST', `/runs/${encodeURIComponent(id)}/tests`, { totals });
452
+ }
424
453
  addRunNote(text, options = {}) {
425
454
  const id = options.run ?? this.requireRun();
426
455
  return this.request('POST', `/runs/${encodeURIComponent(id)}/notes`, { text, failed: options.failed ?? false, address: options.address });
@@ -0,0 +1,11 @@
1
+ import type { FullResult, Reporter, Suite } from '@playwright/test/reporter';
2
+ import { type ClientOptions } from './index.js';
3
+ export default class ProxyMailQAReporter implements Reporter {
4
+ private readonly options;
5
+ private suite;
6
+ constructor(options?: Pick<ClientOptions, 'apiKey' | 'baseUrl'>);
7
+ onBegin(_config: unknown, suite: Suite): void;
8
+ onEnd(_result: FullResult): Promise<void>;
9
+ printsToStdio(): boolean;
10
+ private client;
11
+ }
@@ -0,0 +1,64 @@
1
+ import { ProxyMailQA } from './index.js';
2
+ import { testName } from './test-name.js';
3
+ // Enough for a report; the rest are in Playwright's own.
4
+ const MAX_RESULTS = 200;
5
+ export default class ProxyMailQAReporter {
6
+ options;
7
+ suite = null;
8
+ constructor(options = {}) {
9
+ this.options = options;
10
+ }
11
+ onBegin(_config, suite) {
12
+ this.suite = suite;
13
+ }
14
+ async onEnd(_result) {
15
+ const qa = this.client();
16
+ if (!qa || !this.suite)
17
+ return;
18
+ const totals = { passed: 0, failed: 0, skipped: 0, flaky: 0 };
19
+ const results = [];
20
+ for (const test of this.suite.allTests()) {
21
+ const outcome = test.outcome();
22
+ if (outcome === 'expected')
23
+ totals.passed++;
24
+ else if (outcome === 'skipped')
25
+ totals.skipped++;
26
+ else {
27
+ const flaky = outcome === 'flaky';
28
+ if (flaky)
29
+ totals.flaky++;
30
+ else
31
+ totals.failed++;
32
+ const last = test.results.at(-1);
33
+ results.push({
34
+ name: nameOf(test),
35
+ status: flaky ? 'flaky' : 'failed',
36
+ durationMs: last?.duration,
37
+ error: flaky ? undefined : last?.error?.message,
38
+ });
39
+ }
40
+ }
41
+ // Failures never fail the test run itself: the report is evidence.
42
+ for (const result of results.slice(0, MAX_RESULTS))
43
+ await qa.recordTest(result).catch(() => { });
44
+ await qa.recordTestTotals(totals).catch(() => { });
45
+ }
46
+ printsToStdio() {
47
+ return false;
48
+ }
49
+ client() {
50
+ try {
51
+ const qa = new ProxyMailQA(this.options);
52
+ return qa.run ? qa : null;
53
+ }
54
+ catch {
55
+ // No API key: nothing to report to.
56
+ return null;
57
+ }
58
+ }
59
+ }
60
+ // titlePath: the root, the project, the file, then describe blocks and the title.
61
+ function nameOf(test) {
62
+ const [, project = '', , ...path] = test.titlePath();
63
+ return testName(project, path);
64
+ }
@@ -0,0 +1,5 @@
1
+ import { ProxyMailQA } from './index.js';
2
+ export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
3
+ qa: ProxyMailQA;
4
+ }, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions>;
5
+ export { expect } from '@playwright/test';
@@ -0,0 +1,25 @@
1
+ // Playwright tests with Proxy Mail QA. The qa fixture is a client for the
2
+ // test it's in: in a run (PROXYMAIL_QA_RUN, set by the GitHub Action or by
3
+ // startRun in globalSetup), each call says which test it belongs to, so the
4
+ // run's report shows which test each email came from.
5
+ //
6
+ // import { test, expect } from 'proxymail-qa/playwright';
7
+ //
8
+ // test('sign-up sends one code', async ({ page, qa }) => {
9
+ // const { address } = await qa.createAddress();
10
+ // …
11
+ // });
12
+ //
13
+ // Add proxymail-qa/playwright/reporter to the config's reporters to record
14
+ // failed tests and the totals in the run too.
15
+ import { test as base } from '@playwright/test';
16
+ import { ProxyMailQA } from './index.js';
17
+ import { testName } from './test-name.js';
18
+ export const test = base.extend({
19
+ // eslint-disable-next-line no-empty-pattern
20
+ qa: async ({}, use, testInfo) => {
21
+ // titlePath starts with the file.
22
+ await use(new ProxyMailQA().forTest(testName(testInfo.project.name, testInfo.titlePath.slice(1))));
23
+ },
24
+ });
25
+ export { expect } from '@playwright/test';
@@ -0,0 +1 @@
1
+ export declare const testName: (project: string, path: readonly string[]) => string;
@@ -0,0 +1,3 @@
1
+ // A test's name as the run shows it, the same in the qa fixture and the
2
+ // reporter: the project (when named), the describe blocks and the title.
3
+ export const testName = (project, path) => [project, ...path].filter(Boolean).join(' › ');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proxymail-qa",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Disposable test inboxes for Playwright, Cypress and scripts: create an address, then wait for its verification code or link.",
5
5
  "license": "MIT",
6
6
  "author": "Proxy Mail QA <hello@proxymail.qa> (https://proxymail.qa)",
@@ -29,6 +29,14 @@
29
29
  ".": {
30
30
  "types": "./dist/index.d.ts",
31
31
  "default": "./dist/index.js"
32
+ },
33
+ "./playwright": {
34
+ "types": "./dist/playwright.d.ts",
35
+ "default": "./dist/playwright.js"
36
+ },
37
+ "./playwright/reporter": {
38
+ "types": "./dist/playwright-reporter.d.ts",
39
+ "default": "./dist/playwright-reporter.js"
32
40
  }
33
41
  },
34
42
  "files": [
@@ -46,7 +54,16 @@
46
54
  "typecheck": "tsc -p tsconfig.json --noEmit",
47
55
  "prepublishOnly": "npm run build && npm test"
48
56
  },
57
+ "peerDependencies": {
58
+ "@playwright/test": ">=1.40.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@playwright/test": {
62
+ "optional": true
63
+ }
64
+ },
49
65
  "devDependencies": {
66
+ "@playwright/test": "^1.64.1",
50
67
  "typescript": "^5.6.0",
51
68
  "vitest": "^5.0.3"
52
69
  }