waygraph 0.15.40 → 0.15.42
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 +1 -0
- package/dist/index.js +1 -0
- package/dist/practices-check.d.ts +1 -1
- package/dist/practices-check.js +44 -2
- package/dist/selector.d.ts +50 -0
- package/dist/selector.js +69 -0
- package/package.json +1 -1
- package/templates/skills/waygraph-convention.skill.md +8 -1
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";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type PracticeKind = "wildcard-checkpoint" | "assert-no-type-arg" | "multi-input" | "combined-action" | "empty-verify";
|
|
1
|
+
export type PracticeKind = "wildcard-checkpoint" | "assert-no-type-arg" | "multi-input" | "combined-action" | "empty-verify" | "overcomplex-selector" | "inline-selector-should-use-sel-file";
|
|
2
2
|
export interface PracticeWarning {
|
|
3
3
|
kind: PracticeKind;
|
|
4
4
|
file: string;
|
package/dist/practices-check.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* that compile but erode the typed graph. Warnings only (never fail exit code)
|
|
4
4
|
* unless paired with `tsc` errors in `waygraph typecheck`.
|
|
5
5
|
*/
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
7
|
-
import { relative } from "node:path";
|
|
6
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
7
|
+
import { dirname, relative } from "node:path";
|
|
8
8
|
/** define*Block<…Checkpoint<string>…> — prefer project Checkpoint types. */
|
|
9
9
|
const WILDCARD_CHECKPOINT = /\bdefine(?:Method|Effect|Page|Nav|MemNav|NavClick|Action)Block\s*<[^>]*\bCheckpoint\s*<\s*string\s*>/;
|
|
10
10
|
/** defineAssertBlock({ … }) with no explicit Out type — defaults to wildcard. */
|
|
@@ -23,6 +23,19 @@ const EMPTY_VERIFY_ARRAY = /\bverify\s*:\s*\[\s*\]/;
|
|
|
23
23
|
/** Any `verify:` key at all, empty or not. */
|
|
24
24
|
const HAS_VERIFY_KEY = /\bverify\s*:/;
|
|
25
25
|
const SCAN_GLOB = /\.(block|flow)\.ts$/;
|
|
26
|
+
/**
|
|
27
|
+
* A selector string reaching for a regex-based Playwright text pseudo-class (`:has-text(/…/)`,
|
|
28
|
+
* `:text-matches(…)`) or a plain XPath - real, recurring pattern: an LLM asked to target a simple
|
|
29
|
+
* element (one class, one id, one data-attribute) instead writes a regex/XPath from scratch. Not a
|
|
30
|
+
* type error (Playwright's own selector syntax accepts all of this), so nothing else catches it.
|
|
31
|
+
*/
|
|
32
|
+
const TEXT_MATCHES_PSEUDO = /:text-matches\(/;
|
|
33
|
+
const HAS_TEXT_REGEX_PSEUDO = /:has-text\(\s*\//;
|
|
34
|
+
const XPATH_LOCATOR = /\.locator\(\s*["'`](?:xpath=)?\/\//;
|
|
35
|
+
/** A selector string literal inline in a Block file (`.locator("…")`, `click: "…"`, `url: "…"`) -
|
|
36
|
+
* scoped to simple, single-token selectors (id/class/attr/tag) so this doesn't fire on the
|
|
37
|
+
* legitimate multi-part selectors `.sel.ts` files themselves are made of. */
|
|
38
|
+
const INLINE_SIMPLE_SELECTOR = /(?:\.locator|\bclick|\burl)\s*[:(]\s*["'`](#|\.)[\w-]+["'`]/;
|
|
26
39
|
const METHOD_OR_EFFECT_FILE = /\.(method|effect)\.block\.ts$/;
|
|
27
40
|
const METHOD_OR_EFFECT_DEFINE = /\bdefine(?:Method|Effect)Block\b/;
|
|
28
41
|
/** Whole-file opt-out: `// waygraph-ignore-practices` */
|
|
@@ -107,6 +120,31 @@ export function collectPracticeWarnings(projectDir, walkDir, opts) {
|
|
|
107
120
|
});
|
|
108
121
|
}
|
|
109
122
|
}
|
|
123
|
+
if (!isIgnored("overcomplex-selector", ignore) &&
|
|
124
|
+
(TEXT_MATCHES_PSEUDO.test(src) || HAS_TEXT_REGEX_PSEUDO.test(src) || XPATH_LOCATOR.test(src))) {
|
|
125
|
+
const via = TEXT_MATCHES_PSEUDO.test(src)
|
|
126
|
+
? ":text-matches(...)"
|
|
127
|
+
: HAS_TEXT_REGEX_PSEUDO.test(src)
|
|
128
|
+
? ":has-text(/regex/)"
|
|
129
|
+
: "an XPath locator";
|
|
130
|
+
warnings.push({
|
|
131
|
+
kind: "overcomplex-selector",
|
|
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",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
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"));
|
|
139
|
+
if (siblingSelFile) {
|
|
140
|
+
warnings.push({
|
|
141
|
+
kind: "inline-selector-should-use-sel-file",
|
|
142
|
+
file: rel,
|
|
143
|
+
detail: "a selector string is inlined here, but this directory already has a *.sel.ts file — " +
|
|
144
|
+
"add/reuse the selector there instead of a new one-off literal",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
110
148
|
if (!isIgnored("empty-verify", ignore) && IS_ASSERT_BLOCK.test(src) && EMPTY_VERIFY_ARRAY.test(src)) {
|
|
111
149
|
warnings.push({
|
|
112
150
|
kind: "empty-verify",
|
|
@@ -130,6 +168,10 @@ export function practiceKindLabel(kind) {
|
|
|
130
168
|
return "fill and click in one Block";
|
|
131
169
|
case "empty-verify":
|
|
132
170
|
return "empty verify — transition unconfirmed";
|
|
171
|
+
case "overcomplex-selector":
|
|
172
|
+
return "regex/XPath selector where a plain one would do";
|
|
173
|
+
case "inline-selector-should-use-sel-file":
|
|
174
|
+
return "inline selector — a .sel.ts file already exists here";
|
|
133
175
|
default: {
|
|
134
176
|
const _exhaustive = kind;
|
|
135
177
|
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;
|
package/dist/selector.js
ADDED
|
@@ -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
|
@@ -19,7 +19,14 @@ against a live site, use `--skill-pilot-blind`.
|
|
|
19
19
|
(no goto) even when the app changes URL as a side effect of a button click.
|
|
20
20
|
3. **Selectors in `*Sel`, never inline.** DOM strings live in a `*Sel` next to the page
|
|
21
21
|
(static + `(id) => …`). Mem keys store values, not selectors. `waygraph check` warns on
|
|
22
|
-
inline selector literals in `Trait.visible` / `Trait.text
|
|
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`.
|
|
24
|
+
**Plain CSS first, always.** An id, a class, or `[data-testid=…]` is the selector for
|
|
25
|
+
almost everything real apps have - reach for that before anything else. Don't invent a
|
|
26
|
+
regex/XPath match (`:has-text(/…/)`, `:text-matches(…)`, an XPath locator) for something a
|
|
27
|
+
one-line class or id selector already targets uniquely; `waygraph check` flags this too.
|
|
28
|
+
A regex/text match is for a genuinely unstable DOM with no stable attribute at all, not
|
|
29
|
+
the default first move.
|
|
23
30
|
4. **Assert-only = `defineAssertBlock`.** Self-loop, no hand-written `act`/`resolve`. Pass
|
|
24
31
|
an explicit type argument when it sits between two specifically-typed Blocks in a Flow.
|
|
25
32
|
Not just static content or an action's result - it also names a *feature state* worth a
|