@sdods/core 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/analyze/detectors.js +236 -24
  3. package/dist/analyze/index.d.ts +0 -1
  4. package/dist/analyze/index.js +0 -1
  5. package/dist/analyze/propose.js +80 -38
  6. package/dist/analyze/scan.js +19 -1
  7. package/dist/api/client.js +7 -1
  8. package/dist/auth/capture.js +19 -6
  9. package/dist/auth/index.js +23 -2
  10. package/dist/config/resolve.d.ts +13 -0
  11. package/dist/config/resolve.js +1 -0
  12. package/dist/config/runner.js +2 -2
  13. package/dist/config/tags.d.ts +29 -1
  14. package/dist/config/tags.js +46 -0
  15. package/dist/data/provider.js +5 -1
  16. package/dist/data/user-pool.js +35 -3
  17. package/dist/fixtures/api-context.d.ts +14 -1
  18. package/dist/fixtures/api-context.js +13 -0
  19. package/dist/fixtures/scenario.js +1 -4
  20. package/dist/fixtures/test.js +42 -1
  21. package/dist/fixtures/types.d.ts +2 -0
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/reporters/dashboard.d.ts +86 -0
  25. package/dist/reporters/dashboard.js +319 -61
  26. package/dist/shots/hooks.js +0 -10
  27. package/dist/steps/a11y.steps.d.ts +180 -0
  28. package/dist/steps/a11y.steps.js +598 -0
  29. package/dist/steps/api.steps.js +5 -1
  30. package/dist/steps/browser.steps.d.ts +27 -0
  31. package/dist/steps/browser.steps.js +653 -0
  32. package/dist/steps/clock.steps.d.ts +4 -0
  33. package/dist/steps/clock.steps.js +73 -0
  34. package/dist/steps/data.steps.js +50 -2
  35. package/dist/steps/db.steps.d.ts +5 -0
  36. package/dist/steps/db.steps.js +105 -0
  37. package/dist/steps/dom.steps.d.ts +2 -0
  38. package/dist/steps/dom.steps.js +583 -0
  39. package/dist/steps/glob.d.ts +16 -1
  40. package/dist/steps/glob.js +40 -1
  41. package/dist/steps/iframe.steps.d.ts +2 -0
  42. package/dist/steps/iframe.steps.js +93 -0
  43. package/dist/steps/index.d.ts +11 -1
  44. package/dist/steps/index.js +11 -1
  45. package/dist/steps/net.steps.d.ts +63 -0
  46. package/dist/steps/net.steps.js +728 -0
  47. package/dist/steps/perf.steps.d.ts +248 -0
  48. package/dist/steps/perf.steps.js +514 -0
  49. package/dist/steps/tabs.steps.d.ts +5 -0
  50. package/dist/steps/tabs.steps.js +109 -0
  51. package/dist/steps/webhook.steps.d.ts +46 -0
  52. package/dist/steps/webhook.steps.js +129 -0
  53. package/package.json +3 -4
  54. package/dist/analyze/modules.d.ts +0 -74
  55. package/dist/analyze/modules.js +0 -353
  56. package/dist/config/playwright.d.ts +0 -37
  57. package/dist/config/playwright.js +0 -262
@@ -1,5 +1,7 @@
1
+ import { readdirSync } from 'node:fs';
1
2
  import { dirname } from 'node:path';
2
3
  import { fileURLToPath } from 'node:url';
4
+ import { SdodsError } from '../errors.js';
3
5
  /**
4
6
  * Absolute glob of the core step library. Built from this file's real path so Playwright's
5
7
  * TS transform sees a workspace path (not node_modules) and playwright-bdd loads it.
@@ -7,6 +9,13 @@ import { fileURLToPath } from 'node:url';
7
9
  export function coreStepsDir() {
8
10
  return dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/');
9
11
  }
12
+ /** The library names present in this build — `a11y` for `a11y.steps.ts`. */
13
+ export function coreStepNames() {
14
+ return readdirSync(coreStepsDir())
15
+ .map((f) => /^(.+)\.steps\.(?:js|ts)$/.exec(f)?.[1])
16
+ .filter((n) => Boolean(n))
17
+ .sort();
18
+ }
10
19
  /**
11
20
  * Both extensions, because this resolves against whichever copy of the package is running: the
12
21
  * workspace checkout ships `*.steps.ts`, the published package ships the compiled `*.steps.js`.
@@ -14,8 +23,38 @@ export function coreStepsDir() {
14
23
  * outside the pattern. playwright-bdd passes explicit patterns to tinyglobby untouched — it only
15
24
  * appends extensions to bare directory patterns — and neither it nor tinyglobby excludes
16
25
  * node_modules, so the published layout globs fine.
17
- */
26
+ * */
18
27
  export function coreStepsGlob() {
19
28
  return `${coreStepsDir()}/*.steps.{js,ts}`;
20
29
  }
30
+ /**
31
+ * The patterns bddgen should load, honouring `steps.core.exclude`.
32
+ *
33
+ * Separate from `coreStepsGlob()` because that one is exported API and returns a single string;
34
+ * widening its return type would break every caller for the sake of an option most projects
35
+ * never set. With nothing excluded this returns exactly that same glob, so the common path is
36
+ * byte-identical to what shipped before.
37
+ *
38
+ * An unknown name THROWS rather than being ignored. A silent no-op on a typo would leave the
39
+ * project believing it had excluded a library while the collision it was avoiding still fails
40
+ * generation — and the error it would then read points at the step, not at the typo.
41
+ */
42
+ export function coreStepsPatterns(exclude = []) {
43
+ if (exclude.length === 0)
44
+ return [coreStepsGlob()];
45
+ const available = coreStepNames();
46
+ const unknown = exclude.filter((n) => !available.includes(n));
47
+ if (unknown.length) {
48
+ throw new SdodsError('CONFIG_INVALID', `steps.core.exclude names ${unknown.length === 1 ? 'a library' : 'libraries'} that ` +
49
+ `${unknown.length === 1 ? 'does' : 'do'} not exist: ${unknown.join(', ')}.`, {
50
+ hint: `Available: ${available.join(', ')}. Names are the file basenames, so "a11y" for a11y.steps.ts.`,
51
+ });
52
+ }
53
+ // Explicit per-library patterns rather than a negated glob: playwright-bdd hands these to
54
+ // tinyglobby verbatim, and a `!`-prefixed entry in that list is treated as a path, not an
55
+ // exclusion.
56
+ return available
57
+ .filter((n) => !exclude.includes(n))
58
+ .map((n) => `${coreStepsDir()}/${n}.steps.{js,ts}`);
59
+ }
21
60
  //# sourceMappingURL=glob.js.map
@@ -0,0 +1,2 @@
1
+ import './params.js';
2
+ //# sourceMappingURL=iframe.steps.d.ts.map
@@ -0,0 +1,93 @@
1
+ import { expect } from '@playwright/test';
2
+ import './params.js';
3
+ import { Given, Then, When } from '../fixtures/test.js';
4
+ import { render } from '../api/template.js';
5
+ import { SdodsError } from '../errors.js';
6
+ /**
7
+ * Iframe steps.
8
+ *
9
+ * DESIGN — a frame is entered, not passed around. Every other UI step in this
10
+ * library resolves against `page`, and threading an optional frame through all
11
+ * of them would touch every signature for a surface most scenarios never see.
12
+ * So a scenario *enters* a frame, the subsequent steps here operate inside it,
13
+ * and it *leaves*. The scope is per-scenario state, held on the page object
14
+ * rather than in module scope, so parallel workers cannot see each other's.
15
+ *
16
+ * WHY THIS EXISTS — a cross-origin payment frame is the single most common
17
+ * thing a suite cannot reach, and "we do not test checkout" is not a decision
18
+ * anybody made; it is what happens when the vocabulary has no word for it.
19
+ */
20
+ const scopesOf = (apiContext, env) => [apiContext.vars.toObject(), env.vars];
21
+ /** Per-page frame scope. A WeakMap, so a closed page's entry is collectable. */
22
+ const entered = new WeakMap();
23
+ function currentFrame(page) {
24
+ const scope = entered.get(page);
25
+ if (!scope) {
26
+ throw new SdodsError('NOT_SUPPORTED', 'No iframe has been entered.', {
27
+ hint: 'Use `Given I enter the frame "<selector or name>"` before addressing elements inside it.',
28
+ });
29
+ }
30
+ return scope.frame;
31
+ }
32
+ /* ── entering and leaving ─────────────────────────────────────────────── */
33
+ Given('I enter the frame {string}', async ({ page, apiContext, env }, selector) => {
34
+ const resolved = render(selector, ...scopesOf(apiContext, env));
35
+ // A bare word is treated as a name/title/id, which is how frames are
36
+ // usually identified in markup; anything else is a CSS selector.
37
+ const css = /^[\w-]+$/.test(resolved)
38
+ ? `iframe[name="${resolved}"], iframe[title="${resolved}"], iframe#${resolved}`
39
+ : resolved;
40
+ const handle = page.locator(css).first();
41
+ // Assert the element EXISTS before entering. `frameLocator` on a selector
42
+ // that matches nothing fails later, inside an unrelated step, with a
43
+ // message about the element you were looking for rather than the frame you
44
+ // never entered.
45
+ await expect(handle, `the frame "${resolved}" should be present before entering it`).toBeAttached();
46
+ entered.set(page, { frame: page.frameLocator(css), description: resolved });
47
+ });
48
+ Given('I leave the frame', async ({ page }) => {
49
+ entered.delete(page);
50
+ });
51
+ /* ── acting inside the frame ──────────────────────────────────────────── */
52
+ When('I fill the frame field {string} with {string}', async ({ page, apiContext, env }, label, value) => {
53
+ const scopes = scopesOf(apiContext, env);
54
+ const frame = currentFrame(page);
55
+ await frame
56
+ .getByLabel(render(label, ...scopes))
57
+ .or(frame.getByPlaceholder(render(label, ...scopes)))
58
+ .first()
59
+ .fill(render(value, ...scopes));
60
+ });
61
+ When('I click the frame element {string}', async ({ page, apiContext, env }, selector) => {
62
+ await currentFrame(page)
63
+ .locator(render(selector, ...scopesOf(apiContext, env)))
64
+ .first()
65
+ .click();
66
+ });
67
+ When('I click the frame {role} {string}', async ({ page, apiContext, env }, role, name) => {
68
+ await currentFrame(page)
69
+ .getByRole(role, { name: render(name, ...scopesOf(apiContext, env)) })
70
+ .first()
71
+ .click();
72
+ });
73
+ /* ── asserting inside the frame ───────────────────────────────────────── */
74
+ Then('the frame should contain the text {string}', async ({ page, apiContext, env }, text) => {
75
+ await expect(currentFrame(page)
76
+ .getByText(render(text, ...scopesOf(apiContext, env)))
77
+ .first()).toBeVisible();
78
+ });
79
+ Then('the frame element {string} should be visible', async ({ page, apiContext, env }, selector) => {
80
+ await expect(currentFrame(page)
81
+ .locator(render(selector, ...scopesOf(apiContext, env)))
82
+ .first()).toBeVisible();
83
+ });
84
+ Then('the page should have {int} frame(s)', async ({ page }, count) => {
85
+ // `page.frames()` includes the main frame; the count a scenario means is the
86
+ // number of embedded documents, so the main frame is excluded.
87
+ await expect
88
+ .poll(() => page.frames().length - 1, {
89
+ message: `the page should embed ${count} frame(s)`,
90
+ })
91
+ .toBe(count);
92
+ });
93
+ //# sourceMappingURL=iframe.steps.js.map
@@ -7,6 +7,16 @@ import './api.steps.js';
7
7
  import './ui.steps.js';
8
8
  import './data.steps.js';
9
9
  import './hybrid.steps.js';
10
+ import './iframe.steps.js';
11
+ import './tabs.steps.js';
12
+ import './db.steps.js';
13
+ import './clock.steps.js';
14
+ import './webhook.steps.js';
15
+ import './a11y.steps.js';
16
+ import './browser.steps.js';
17
+ import './dom.steps.js';
18
+ import './net.steps.js';
19
+ import './perf.steps.js';
10
20
  import '../shots/hooks.js';
11
- export { coreStepsGlob, coreStepsDir } from './glob.js';
21
+ export { coreStepsGlob, coreStepsPatterns, coreStepNames, coreStepsDir } from './glob.js';
12
22
  //# sourceMappingURL=index.d.ts.map
@@ -7,6 +7,16 @@ import './api.steps.js';
7
7
  import './ui.steps.js';
8
8
  import './data.steps.js';
9
9
  import './hybrid.steps.js';
10
+ import './iframe.steps.js';
11
+ import './tabs.steps.js';
12
+ import './db.steps.js';
13
+ import './clock.steps.js';
14
+ import './webhook.steps.js';
15
+ import './a11y.steps.js';
16
+ import './browser.steps.js';
17
+ import './dom.steps.js';
18
+ import './net.steps.js';
19
+ import './perf.steps.js';
10
20
  import '../shots/hooks.js';
11
- export { coreStepsGlob, coreStepsDir } from './glob.js';
21
+ export { coreStepsGlob, coreStepsPatterns, coreStepNames, coreStepsDir } from './glob.js';
12
22
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,63 @@
1
+ import './params.js';
2
+ import type { APIRequestContext, TestInfo } from '@playwright/test';
3
+ import type { EnvConfig } from '@sdods/contracts';
4
+ import type { ResolvedConfig } from '../config/resolve.js';
5
+ import type { ApiContext } from '../fixtures/api-context.js';
6
+ export interface RawSnapshot {
7
+ method: string;
8
+ url: string;
9
+ requestBody?: string;
10
+ status: number;
11
+ statusText: string;
12
+ /** Lower-cased, comma-joined by Playwright for repeats — use `setCookies` for Set-Cookie. */
13
+ headers: Record<string, string>;
14
+ /** Every individual Set-Cookie line, which `headers()` would have collapsed into one string. */
15
+ setCookies: string[];
16
+ body: string;
17
+ }
18
+ export interface SendRawOptions {
19
+ body?: string;
20
+ form?: Record<string, string>;
21
+ }
22
+ /**
23
+ * One request with `maxRedirects: 0`. The whole point is to keep the 3xx instead of chasing it,
24
+ * and to keep `Set-Cookie` unredacted — the two things the recorded ApiSnapshot cannot carry.
25
+ */
26
+ export declare function sendRaw(deps: {
27
+ request: APIRequestContext;
28
+ apiContext: ApiContext;
29
+ env: EnvConfig;
30
+ config: ResolvedConfig;
31
+ testInfo?: Pick<TestInfo, 'attach'>;
32
+ }, method: string, pathOrUrl: string, opts?: SendRawOptions): Promise<RawSnapshot>;
33
+ export declare function redactForReport(headers: Record<string, string>): Record<string, string>;
34
+ export interface StreamEvent {
35
+ /** The SSE `event:` field, else the parsed JSON payload's `type`, else the raw sentinel. */
36
+ type: string | undefined;
37
+ data: string;
38
+ json?: unknown;
39
+ }
40
+ export interface StreamCapture {
41
+ status: number;
42
+ contentType: string;
43
+ events: StreamEvent[];
44
+ /** The body ended on a frame boundary — a stream cut mid-frame does not. */
45
+ endedWithDelimiter: boolean;
46
+ /** Transport failure (reset, timeout). Present means the stream did not finish. */
47
+ error?: string;
48
+ }
49
+ /**
50
+ * Parse an SSE body into frames. Frames are separated by a blank line; `data:` lines within one
51
+ * frame join with a newline; a frame carrying only a comment (`: keepalive`) is not an event.
52
+ */
53
+ export declare function parseEventStream(text: string): Pick<StreamCapture, 'events' | 'endedWithDelimiter'>;
54
+ /**
55
+ * Playwright `page.route` glob semantics, restricted to the three operators that matter:
56
+ * `**` matches anything including `/`, `*` matches anything except `/`, `?` matches one
57
+ * non-`/` character. Anchored, like Playwright's own matcher. Kept deliberately compatible so a
58
+ * counting step and a `I mock {string} …` step can be given the same string.
59
+ */
60
+ export declare function globToRegExp(glob: string): RegExp;
61
+ /** Every credential shape found in `text`, named. Exported so the patterns are testable. */
62
+ export declare function credentialFindings(text: string): string[];
63
+ //# sourceMappingURL=net.steps.d.ts.map