waygraph 0.15.41 → 0.15.43

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.d.ts CHANGED
@@ -2,6 +2,7 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInsta
2
2
  export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { registerMemStub, seedMemStub, getMemStub } from "./mem-stub.js";
5
+ export { css, byText, byRole } from "./selector.js";
5
6
  export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
6
7
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, branchRoutes, collectBranchFlows, runBranchRegression, } from "./engine.js";
7
8
  export type { BranchEntry, BranchRunResult, RunBranchRegressionOptions } from "./engine/branch-regression.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
3
  export { registerMemStub, seedMemStub, getMemStub } from "./mem-stub.js";
4
+ export { css, byText, byRole } from "./selector.js";
4
5
  export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
5
6
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, branchRoutes, collectBranchFlows, runBranchRegression, } from "./engine.js";
6
7
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
@@ -130,17 +130,19 @@ export function collectPracticeWarnings(projectDir, walkDir, opts) {
130
130
  warnings.push({
131
131
  kind: "overcomplex-selector",
132
132
  file: rel,
133
- detail: `uses ${via} — reach for a plain CSS selector (id/class/data-attribute, via a .sel.ts ` +
134
- "export) instead of a regex/XPath match unless the DOM genuinely has no stable selector",
133
+ detail: `uses ${via} — reach for a plain CSS selector (id/class/data-attribute, via a _sel.ts/` +
134
+ "*.sel.ts export) instead of a regex/XPath match unless the DOM genuinely has no stable selector",
135
135
  });
136
136
  }
137
137
  if (!isIgnored("inline-selector-should-use-sel-file", ignore) && INLINE_SIMPLE_SELECTOR.test(src)) {
138
- const siblingSelFile = readdirSync(dirname(file)).some((f) => f.endsWith(".sel.ts"));
138
+ // Two real, coexisting conventions: the Map layout's own `_sel.ts` (one per page folder, next
139
+ // to `_nav`/`_page`/`_methods`) and the freeform layout's per-concern `<name>.sel.ts`.
140
+ const siblingSelFile = readdirSync(dirname(file)).some((f) => f === "_sel.ts" || f.endsWith(".sel.ts"));
139
141
  if (siblingSelFile) {
140
142
  warnings.push({
141
143
  kind: "inline-selector-should-use-sel-file",
142
144
  file: rel,
143
- detail: "a selector string is inlined here, but this directory already has a *.sel.ts file — " +
145
+ detail: "a selector string is inlined here, but this directory already has a _sel.ts / *.sel.ts file — " +
144
146
  "add/reuse the selector there instead of a new one-off literal",
145
147
  });
146
148
  }
@@ -171,7 +173,7 @@ export function practiceKindLabel(kind) {
171
173
  case "overcomplex-selector":
172
174
  return "regex/XPath selector where a plain one would do";
173
175
  case "inline-selector-should-use-sel-file":
174
- return "inline selector — a .sel.ts file already exists here";
176
+ return "inline selector — a _sel.ts/*.sel.ts file already exists here";
175
177
  default: {
176
178
  const _exhaustive = kind;
177
179
  return _exhaustive;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Thin selector helpers - the obvious, discoverable shape for the plain-CSS-first rule
3
+ * (`waygraph-convention.skill.md` #3, `practices-check.ts`'s `overcomplex-selector` warning).
4
+ * Every `selector: string` in the engine (Trait factories, Block `click`/`url`, `.sel.ts` exports)
5
+ * already accepts a plain string - these don't add a new selector TYPE, they add three obvious
6
+ * *functions* to reach for instead of hand-writing a regex/XPath/text-match locator from scratch.
7
+ *
8
+ * `css()` also validates: it throws immediately (not just a lint warning later) if you hand it a
9
+ * regex-looking or XPath string, so the mistake this file exists to steer away from fails loud at
10
+ * the call site instead of quietly working via a needlessly complex selector.
11
+ *
12
+ * A full generated registry of a project's own `*Sel` selectors (Adonis/Prisma-style - typed
13
+ * autocomplete over selectors that already exist, not just these three shapes) is real, larger,
14
+ * future work - backlogged until waygraph's own bigger step (a Dagster/Airflow-style run UI), not
15
+ * part of this pass.
16
+ */
17
+ /**
18
+ * A plain CSS selector - id, class, tag, attribute, or any combination Playwright's own CSS engine
19
+ * accepts. Throws if `raw` looks like a regex text-match pseudo-class or an XPath instead of CSS -
20
+ * use {@link byText} for a genuine text match, or a raw string (still perfectly valid) when the DOM
21
+ * truly has no stable selector and a regex/XPath is the only option.
22
+ * @example css("#login-button")
23
+ * @example css(".inventory_item .btn_primary")
24
+ */
25
+ export declare function css(raw: string): string;
26
+ /**
27
+ * A text-match selector, scoped to `within` (a plain CSS selector) when given. Exact match by
28
+ * default - pass `{ contains: true }` for a substring match (Playwright's own `:has-text()`).
29
+ * Prefer this over hand-writing `:has-text(/regex/)` when a plain string is all you need.
30
+ * @example byText("Login") // any element with exactly this text
31
+ * @example byText("Submit", { within: "form" }) // scoped to a plain CSS selector
32
+ * @example byText("out of stock", { contains: true }) // substring match
33
+ */
34
+ export declare function byText(text: string, opts?: {
35
+ within?: string;
36
+ contains?: boolean;
37
+ }): string;
38
+ /**
39
+ * An ARIA role selector (Playwright's own `role=` engine) - the accessible-name-first way to
40
+ * target a control, ahead of a brittle DOM-structure guess. `name` matches the element's
41
+ * accessible name exactly (whitespace-normalized), same as Playwright's own role engine default.
42
+ * @example byRole("button", { name: "Login" })
43
+ * @example byRole("checkbox", { name: "Remember me", checked: true })
44
+ */
45
+ export declare function byRole(role: string, opts?: {
46
+ name?: string;
47
+ checked?: boolean;
48
+ disabled?: boolean;
49
+ pressed?: boolean;
50
+ }): string;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Thin selector helpers - the obvious, discoverable shape for the plain-CSS-first rule
3
+ * (`waygraph-convention.skill.md` #3, `practices-check.ts`'s `overcomplex-selector` warning).
4
+ * Every `selector: string` in the engine (Trait factories, Block `click`/`url`, `.sel.ts` exports)
5
+ * already accepts a plain string - these don't add a new selector TYPE, they add three obvious
6
+ * *functions* to reach for instead of hand-writing a regex/XPath/text-match locator from scratch.
7
+ *
8
+ * `css()` also validates: it throws immediately (not just a lint warning later) if you hand it a
9
+ * regex-looking or XPath string, so the mistake this file exists to steer away from fails loud at
10
+ * the call site instead of quietly working via a needlessly complex selector.
11
+ *
12
+ * A full generated registry of a project's own `*Sel` selectors (Adonis/Prisma-style - typed
13
+ * autocomplete over selectors that already exist, not just these three shapes) is real, larger,
14
+ * future work - backlogged until waygraph's own bigger step (a Dagster/Airflow-style run UI), not
15
+ * part of this pass.
16
+ */
17
+ const LOOKS_LIKE_REGEX_TEXT_MATCH = /:text-matches\(|:has-text\(\s*\//;
18
+ const LOOKS_LIKE_XPATH = /^\s*(?:xpath=)?\/\//;
19
+ /**
20
+ * A plain CSS selector - id, class, tag, attribute, or any combination Playwright's own CSS engine
21
+ * accepts. Throws if `raw` looks like a regex text-match pseudo-class or an XPath instead of CSS -
22
+ * use {@link byText} for a genuine text match, or a raw string (still perfectly valid) when the DOM
23
+ * truly has no stable selector and a regex/XPath is the only option.
24
+ * @example css("#login-button")
25
+ * @example css(".inventory_item .btn_primary")
26
+ */
27
+ export function css(raw) {
28
+ if (LOOKS_LIKE_XPATH.test(raw)) {
29
+ throw new Error(`css("${raw}"): looks like an XPath, not a plain CSS selector - use the raw string directly ` +
30
+ "if the DOM genuinely has no stable id/class/attribute (css() is for the common case only).");
31
+ }
32
+ if (LOOKS_LIKE_REGEX_TEXT_MATCH.test(raw)) {
33
+ throw new Error(`css("${raw}"): looks like a regex text-match pseudo-class - use byText() for a plain text ` +
34
+ "match, or the raw string directly if you genuinely need a regex.");
35
+ }
36
+ return raw;
37
+ }
38
+ /**
39
+ * A text-match selector, scoped to `within` (a plain CSS selector) when given. Exact match by
40
+ * default - pass `{ contains: true }` for a substring match (Playwright's own `:has-text()`).
41
+ * Prefer this over hand-writing `:has-text(/regex/)` when a plain string is all you need.
42
+ * @example byText("Login") // any element with exactly this text
43
+ * @example byText("Submit", { within: "form" }) // scoped to a plain CSS selector
44
+ * @example byText("out of stock", { contains: true }) // substring match
45
+ */
46
+ export function byText(text, opts) {
47
+ const escaped = text.replace(/"/g, '\\"');
48
+ const clause = opts?.contains ? `:has-text("${escaped}")` : `:text-is("${escaped}")`;
49
+ return opts?.within ? `${css(opts.within)} ${clause}` : `*${clause}`;
50
+ }
51
+ /**
52
+ * An ARIA role selector (Playwright's own `role=` engine) - the accessible-name-first way to
53
+ * target a control, ahead of a brittle DOM-structure guess. `name` matches the element's
54
+ * accessible name exactly (whitespace-normalized), same as Playwright's own role engine default.
55
+ * @example byRole("button", { name: "Login" })
56
+ * @example byRole("checkbox", { name: "Remember me", checked: true })
57
+ */
58
+ export function byRole(role, opts) {
59
+ const attrs = [];
60
+ if (opts?.name !== undefined)
61
+ attrs.push(`name="${opts.name.replace(/"/g, '\\"')}"`);
62
+ if (opts?.checked !== undefined)
63
+ attrs.push(`checked=${opts.checked}`);
64
+ if (opts?.disabled !== undefined)
65
+ attrs.push(`disabled=${opts.disabled}`);
66
+ if (opts?.pressed !== undefined)
67
+ attrs.push(`pressed=${opts.pressed}`);
68
+ return attrs.length ? `role=${role}[${attrs.join("][")}]` : `role=${role}`;
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.41",
3
+ "version": "0.15.43",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -42,11 +42,13 @@
42
42
  "test": "npm run check:structure && npm run test:unit && playwright test",
43
43
  "test:ui": "playwright test --ui",
44
44
  "test:unit": "node --import tsx/esm --test tests/unit/*.spec.ts",
45
- "docs:preview": "python3 -m http.server 4173 --directory docs",
46
45
  "example:saucedemo:install": "npm --prefix examples/saucedemo install",
47
46
  "example:saucedemo:test": "npm --prefix examples/saucedemo test",
48
47
  "example:saucedemo:auto": "npm --prefix examples/saucedemo run auto",
49
- "check:structure": "node scripts/check-structure.mjs"
48
+ "check:structure": "node scripts/check-structure.mjs",
49
+ "docs:dev": "vitepress dev docs",
50
+ "docs:build": "vitepress build docs",
51
+ "docs:preview": "vitepress preview docs"
50
52
  },
51
53
  "peerDependencies": {
52
54
  "@playwright/test": "^1.40.0"
@@ -58,6 +60,7 @@
58
60
  "devDependencies": {
59
61
  "@playwright/test": "^1.63.0",
60
62
  "@types/node": "^26.0.0",
61
- "typescript": "^5.6.0"
63
+ "typescript": "^5.6.0",
64
+ "vitepress": "^1.6.4"
62
65
  }
63
66
  }
@@ -17,10 +17,12 @@ against a live site, use `--skill-pilot-blind`.
17
17
  2. **Navigation only inside Nav.** `page.goto` / NavBlock `click` live in
18
18
  `defineNavBlock` / `defineNavClickBlock` only. Method/Effect `act` uses ActionPage
19
19
  (no goto) even when the app changes URL as a side effect of a button click.
20
- 3. **Selectors in `*Sel`, never inline.** DOM strings live in a `*Sel` next to the page
21
- (static + `(id) => …`). Mem keys store values, not selectors. `waygraph check` warns on
22
- inline selector literals in `Trait.visible` / `Trait.text`, and separately on any inline
23
- selector literal (`.locator("#x")`, `click: "#x"`) in a directory that already has a `*Sel`.
20
+ 3. **Selectors in `*Sel`, never inline.** DOM strings live in a `*Sel` next to the page - `_sel.ts`
21
+ in the Map layout (one per page folder, next to `_nav`/`_page`/`_methods`), `<name>.sel.ts` in a
22
+ freeform layout (one per concern) - never a fresh file-naming convention per project (`_sel.ts`,
23
+ not `_selectors.ts` or a `_selectors/` folder). Mem keys store values, not selectors. `waygraph
24
+ check` warns on inline selector literals in `Trait.visible` / `Trait.text`, and separately on any
25
+ inline selector literal (`.locator("#x")`, `click: "#x"`) in a directory that already has a `_sel.ts`/`*.sel.ts`.
24
26
  **Plain CSS first, always.** An id, a class, or `[data-testid=…]` is the selector for
25
27
  almost everything real apps have - reach for that before anything else. Don't invent a
26
28
  regex/XPath match (`:has-text(/…/)`, `:text-matches(…)`, an XPath locator) for something a