@ripplo/testing 0.3.2 → 0.3.3

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
@@ -229,9 +229,13 @@ import { extract, variable } from "@ripplo/testing/control";
229
229
 
230
230
  const token = variable("token");
231
231
  extract(testId("token-value"), token).as("capture token");
232
- // token can be referenced in subsequent steps
232
+ // pass the token anywhere a string value is accepted:
233
+ fill(role("textbox", "Paste here"), token).as("paste token");
234
+ assert.value(role("textbox", "Paste here"), token).as("assert token");
233
235
  ```
234
236
 
237
+ Never reference a runtime variable with a literal `"{{vars.name}}"` string — always pass the `Variable` token so TypeScript verifies the reference.
238
+
235
239
  ### Step Labels
236
240
 
237
241
  Every step **must** have a `.as("description")` label:
@@ -260,6 +264,37 @@ test("delete-project")
260
264
 
261
265
  **Always destructure and use precondition data.** Never hardcode values that come from preconditions — if a precondition implementation changes, the test should not break.
262
266
 
267
+ **Never write `"{{namespace.key}}"` as a string literal.** Pass the destructured proxy value directly. Internally the proxy stringifies to `{{namespace.key}}`, but writing the literal bypasses TypeScript so typos (`{{tabel.name}}`) silently compile and silently fail at runtime. The `no-literal-template-strings` lint rule enforces this.
268
+
269
+ ```typescript
270
+ // ❌ WRONG — literal template string, no type-checking
271
+ .steps(({ table }) => [
272
+ assert.value(role("textbox", "Table name"), "{{table.name}}").as("name visible"),
273
+ ])
274
+
275
+ // ✅ CORRECT — proxy value, type-checked against requires()
276
+ .steps(({ table }) => [
277
+ assert.value(role("textbox", "Table name"), table.name).as("name visible"),
278
+ ])
279
+ ```
280
+
281
+ Runtime variables (captured via `clipboard`, `extract`, etc.) follow the same rule. Create a token with `variable("name")` and pass it anywhere a value is accepted:
282
+
283
+ ```typescript
284
+ import { variable } from "@ripplo/testing/control";
285
+
286
+ .steps(() => {
287
+ const copied = variable("copied");
288
+ return [
289
+ click(role("button", "Copy")).as("copy"),
290
+ clipboard({ action: "read", target: copied, value: undefined }).as("read clipboard"),
291
+ assert.value(role("button", "Copy"), copied).as("match clipboard"),
292
+ ];
293
+ })
294
+ ```
295
+
296
+ Never write `"{{vars.copied}}"` as a string literal — use the token.
297
+
263
298
  ## Wiring it together
264
299
 
265
300
  ### `.ripplo/ripplo.ts` — the definitions funnel
package/dist/actions.d.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { Variable, StaticStringRef, VariableRef } from './control.js';
1
2
  import { U as UnlabeledStep } from './step-De52hTLd.js';
2
3
  import { CheckLocator, InputLocator, AnyLocator, SelectLocator } from './locators.js';
3
4
  import '@ripplo/spec';
4
5
 
6
+ type StringOrVariable = string | Variable<string>;
5
7
  declare function navigate(url: string): UnlabeledStep<{
6
8
  type: "goto";
7
9
  url: {
@@ -24,7 +26,7 @@ declare function click(locator: AnyLocator, options?: StepOptions): UnlabeledSte
24
26
  type: "click";
25
27
  uiOnly: boolean | undefined;
26
28
  }>;
27
- declare function fill(locator: InputLocator, value: string): UnlabeledStep<{
29
+ declare function fill(locator: InputLocator, value: StringOrVariable): UnlabeledStep<{
28
30
  locator: {
29
31
  by: "testId";
30
32
  value: string;
@@ -34,12 +36,9 @@ declare function fill(locator: InputLocator, value: string): UnlabeledStep<{
34
36
  name?: string | undefined;
35
37
  };
36
38
  type: "fill";
37
- value: {
38
- type: "static";
39
- value: string;
40
- };
39
+ value: StaticStringRef | VariableRef;
41
40
  }>;
42
- declare function select(locator: SelectLocator, value: string): UnlabeledStep<{
41
+ declare function select(locator: SelectLocator, value: StringOrVariable): UnlabeledStep<{
43
42
  locator: {
44
43
  by: "testId";
45
44
  value: string;
@@ -49,10 +48,7 @@ declare function select(locator: SelectLocator, value: string): UnlabeledStep<{
49
48
  name?: string | undefined;
50
49
  };
51
50
  type: "select";
52
- value: {
53
- type: "static";
54
- value: string;
55
- };
51
+ value: StaticStringRef | VariableRef;
56
52
  }>;
57
53
  declare function check(locator: CheckLocator): UnlabeledStep<{
58
54
  locator: {
@@ -137,7 +133,7 @@ declare function clear(locator: InputLocator): UnlabeledStep<{
137
133
  };
138
134
  type: "clear";
139
135
  }>;
140
- declare function typeText(locator: InputLocator, value: string): UnlabeledStep<{
136
+ declare function typeText(locator: InputLocator, value: StringOrVariable): UnlabeledStep<{
141
137
  locator: {
142
138
  by: "testId";
143
139
  value: string;
@@ -147,10 +143,7 @@ declare function typeText(locator: InputLocator, value: string): UnlabeledStep<{
147
143
  name?: string | undefined;
148
144
  };
149
145
  type: "type";
150
- value: {
151
- type: "static";
152
- value: string;
153
- };
146
+ value: StaticStringRef | VariableRef;
154
147
  }>;
155
148
  declare function rightClick(locator: AnyLocator): UnlabeledStep<{
156
149
  locator: {
@@ -206,16 +199,13 @@ declare function handleDialog({ action, promptText, uiOnly }: HandleDialogOption
206
199
  }>;
207
200
  interface ClipboardOptions {
208
201
  readonly action: "read" | "write";
209
- readonly value: string | undefined;
210
- readonly variable: string | undefined;
202
+ readonly target: Variable<string> | undefined;
203
+ readonly value: StringOrVariable | undefined;
211
204
  }
212
- declare function clipboard({ action, value, variable }: ClipboardOptions): UnlabeledStep<{
205
+ declare function clipboard({ action, target, value }: ClipboardOptions): UnlabeledStep<{
213
206
  action: "read" | "write";
214
207
  type: "clipboard";
215
- value: {
216
- type: "static";
217
- value: string;
218
- } | undefined;
208
+ value: StaticStringRef | VariableRef | undefined;
219
209
  variable: string | undefined;
220
210
  }>;
221
211
  interface SetPermissionOptions {
package/dist/actions.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {
2
- toSpecLocator
3
- } from "./chunk-2VUWFRR5.js";
2
+ readVariable,
3
+ toSpecLocator,
4
+ toStringValueRef
5
+ } from "./chunk-P67G7RA7.js";
4
6
  import {
5
7
  createStep
6
8
  } from "./chunk-MGATMMCZ.js";
@@ -21,14 +23,14 @@ function fill(locator, value) {
21
23
  return createStep({
22
24
  locator: toSpecLocator(locator),
23
25
  type: "fill",
24
- value: { type: "static", value }
26
+ value: toStringValueRef(value)
25
27
  });
26
28
  }
27
29
  function select(locator, value) {
28
30
  return createStep({
29
31
  locator: toSpecLocator(locator),
30
32
  type: "select",
31
- value: { type: "static", value }
33
+ value: toStringValueRef(value)
32
34
  });
33
35
  }
34
36
  function check(locator) {
@@ -64,7 +66,7 @@ function typeText(locator, value) {
64
66
  return createStep({
65
67
  locator: toSpecLocator(locator),
66
68
  type: "type",
67
- value: { type: "static", value }
69
+ value: toStringValueRef(value)
68
70
  });
69
71
  }
70
72
  function rightClick(locator) {
@@ -83,12 +85,12 @@ function drag(source, target) {
83
85
  function handleDialog({ action, promptText, uiOnly }) {
84
86
  return createStep({ action, promptText, type: "handleDialog", uiOnly });
85
87
  }
86
- function clipboard({ action, value, variable }) {
88
+ function clipboard({ action, target, value }) {
87
89
  return createStep({
88
90
  action,
89
91
  type: "clipboard",
90
- value: value == null ? void 0 : { type: "static", value },
91
- variable
92
+ value: value == null ? void 0 : toStringValueRef(value),
93
+ variable: target == null ? void 0 : readVariable(target)
92
94
  });
93
95
  }
94
96
  function setPermission({ permission, state }) {
package/dist/assert.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { O as ObserverHandle, a as ObserverInput, b as ObserverBudgetTier } from './types-Degkxs1f.js';
2
+ import { Variable, StaticStringRef, VariableRef } from './control.js';
2
3
  import { U as UnlabeledStep } from './step-De52hTLd.js';
3
4
  import { CheckLocator, AnyLocator } from './locators.js';
4
5
  import 'zod';
5
6
  import '@ripplo/spec';
6
7
 
8
+ type StringOrVariable = string | Variable<string>;
7
9
  declare const assert: {
8
10
  not: {
9
11
  checked(locator: CheckLocator): UnlabeledStep<{
@@ -51,12 +53,9 @@ declare const assert: {
51
53
  type: "assertNotVisible";
52
54
  }>;
53
55
  };
54
- attribute(locator: AnyLocator, attribute: string, expected: string): UnlabeledStep<{
56
+ attribute(locator: AnyLocator, attribute: string, expected: StringOrVariable): UnlabeledStep<{
55
57
  attribute: string;
56
- expected: {
57
- type: "static";
58
- value: string;
59
- };
58
+ expected: StaticStringRef | VariableRef;
60
59
  locator: {
61
60
  by: "testId";
62
61
  value: string;
@@ -137,11 +136,8 @@ declare const assert: {
137
136
  };
138
137
  type: "assertFocused";
139
138
  }>;
140
- text(locator: AnyLocator, expected: string): UnlabeledStep<{
141
- expected: {
142
- type: "static";
143
- value: string;
144
- };
139
+ text(locator: AnyLocator, expected: StringOrVariable): UnlabeledStep<{
140
+ expected: StaticStringRef | VariableRef;
145
141
  locator: {
146
142
  by: "testId";
147
143
  value: string;
@@ -153,19 +149,13 @@ declare const assert: {
153
149
  operator: "equals";
154
150
  type: "assertText";
155
151
  }>;
156
- url(expected: string): UnlabeledStep<{
157
- expected: {
158
- type: "static";
159
- value: string;
160
- };
152
+ url(expected: StringOrVariable): UnlabeledStep<{
153
+ expected: StaticStringRef | VariableRef;
161
154
  operator: "contains";
162
155
  type: "assertUrl";
163
156
  }>;
164
- value(locator: AnyLocator, expected: string): UnlabeledStep<{
165
- expected: {
166
- type: "static";
167
- value: string;
168
- };
157
+ value(locator: AnyLocator, expected: StringOrVariable): UnlabeledStep<{
158
+ expected: StaticStringRef | VariableRef;
169
159
  locator: {
170
160
  by: "testId";
171
161
  value: string;
package/dist/assert.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
- toSpecLocator
3
- } from "./chunk-2VUWFRR5.js";
2
+ toSpecLocator,
3
+ toStringValueRef
4
+ } from "./chunk-P67G7RA7.js";
4
5
  import {
5
6
  readObserverBudget,
6
7
  readObserverName
@@ -29,7 +30,7 @@ var assert = {
29
30
  attribute(locator, attribute, expected) {
30
31
  return createStep({
31
32
  attribute,
32
- expected: { type: "static", value: expected },
33
+ expected: toStringValueRef(expected),
33
34
  locator: toSpecLocator(locator),
34
35
  operator: "equals",
35
36
  type: "assertAttribute"
@@ -65,7 +66,7 @@ var assert = {
65
66
  },
66
67
  text(locator, expected) {
67
68
  return createStep({
68
- expected: { type: "static", value: expected },
69
+ expected: toStringValueRef(expected),
69
70
  locator: toSpecLocator(locator),
70
71
  operator: "equals",
71
72
  type: "assertText"
@@ -73,14 +74,14 @@ var assert = {
73
74
  },
74
75
  url(expected) {
75
76
  return createStep({
76
- expected: { type: "static", value: expected },
77
+ expected: toStringValueRef(expected),
77
78
  operator: "contains",
78
79
  type: "assertUrl"
79
80
  });
80
81
  },
81
82
  value(locator, expected) {
82
83
  return createStep({
83
- expected: { type: "static", value: expected },
84
+ expected: toStringValueRef(expected),
84
85
  locator: toSpecLocator(locator),
85
86
  operator: "equals",
86
87
  type: "assertValue"
@@ -0,0 +1,58 @@
1
+ import {
2
+ createStep
3
+ } from "./chunk-MGATMMCZ.js";
4
+ import {
5
+ readLocator
6
+ } from "./chunk-DCJBLS2U.js";
7
+
8
+ // src/steps/to-spec-locator.ts
9
+ function toSpecLocator(loc) {
10
+ const spec = readLocator(loc);
11
+ switch (spec.by) {
12
+ case "role": {
13
+ return { by: "role", name: spec.name, role: spec.role };
14
+ }
15
+ case "testId": {
16
+ return { by: "testId", value: spec.value };
17
+ }
18
+ }
19
+ }
20
+
21
+ // src/steps/control.ts
22
+ function variable(name) {
23
+ return { name };
24
+ }
25
+ function readVariable(v) {
26
+ return v.name;
27
+ }
28
+ function isVariable(v) {
29
+ if (typeof v !== "object" || v == null) {
30
+ return false;
31
+ }
32
+ if (!("name" in v)) {
33
+ return false;
34
+ }
35
+ return typeof v.name === "string";
36
+ }
37
+ function toStringValueRef(value) {
38
+ if (typeof value === "string") {
39
+ return { type: "static", value };
40
+ }
41
+ return { name: readVariable(value), type: "variable" };
42
+ }
43
+ function extract(locator, target) {
44
+ return createStep({
45
+ locator: toSpecLocator(locator),
46
+ type: "extractText",
47
+ variable: readVariable(target)
48
+ });
49
+ }
50
+
51
+ export {
52
+ toSpecLocator,
53
+ variable,
54
+ readVariable,
55
+ isVariable,
56
+ toStringValueRef,
57
+ extract
58
+ };
package/dist/control.d.ts CHANGED
@@ -8,6 +8,16 @@ interface Variable<_TName extends string> {
8
8
  }
9
9
  declare function variable<TName extends string>(name: TName): Variable<TName>;
10
10
  declare function readVariable(v: Variable<string>): string;
11
+ declare function isVariable(v: unknown): v is Variable<string>;
12
+ interface StaticStringRef {
13
+ readonly type: "static";
14
+ readonly value: string;
15
+ }
16
+ interface VariableRef {
17
+ readonly name: string;
18
+ readonly type: "variable";
19
+ }
20
+ declare function toStringValueRef(value: string | Variable<string>): StaticStringRef | VariableRef;
11
21
  declare function extract(locator: AnyLocator, target: Variable<string>): UnlabeledStep<{
12
22
  locator: {
13
23
  by: "testId";
@@ -21,4 +31,4 @@ declare function extract(locator: AnyLocator, target: Variable<string>): Unlabel
21
31
  variable: string;
22
32
  }>;
23
33
 
24
- export { type Variable, extract, readVariable, variable };
34
+ export { type StaticStringRef, type Variable, type VariableRef, extract, isVariable, readVariable, toStringValueRef, variable };
package/dist/control.js CHANGED
@@ -1,27 +1,16 @@
1
1
  import {
2
- toSpecLocator
3
- } from "./chunk-2VUWFRR5.js";
4
- import {
5
- createStep
6
- } from "./chunk-MGATMMCZ.js";
2
+ extract,
3
+ isVariable,
4
+ readVariable,
5
+ toStringValueRef,
6
+ variable
7
+ } from "./chunk-P67G7RA7.js";
8
+ import "./chunk-MGATMMCZ.js";
7
9
  import "./chunk-DCJBLS2U.js";
8
-
9
- // src/steps/control.ts
10
- function variable(name) {
11
- return { name };
12
- }
13
- function readVariable(v) {
14
- return v.name;
15
- }
16
- function extract(locator, target) {
17
- return createStep({
18
- locator: toSpecLocator(locator),
19
- type: "extractText",
20
- variable: readVariable(target)
21
- });
22
- }
23
10
  export {
24
11
  extract,
12
+ isVariable,
25
13
  readVariable,
14
+ toStringValueRef,
26
15
  variable
27
16
  };
package/dist/index.js CHANGED
@@ -716,9 +716,33 @@ function observerParamsReferenceVariables(nodes, test2, report) {
716
716
  });
717
717
  });
718
718
  }
719
+ function noLiteralTemplateStrings(nodes, test2, report) {
720
+ const declaredKeys = new Set(Object.keys(test2.spec.variables ?? {}));
721
+ const pattern = /\{\{([^{}]+?)\}\}/g;
722
+ nodes.forEach((node) => {
723
+ const serialized = JSON.stringify(node);
724
+ const allMatches = [...serialized.matchAll(pattern)];
725
+ const undeclared = [
726
+ ...new Set(
727
+ allMatches.map((m) => m[1]).filter((key) => key != null && !declaredKeys.has(key))
728
+ )
729
+ ];
730
+ if (undeclared.length === 0) {
731
+ return;
732
+ }
733
+ const keyList = undeclared.map((k) => `{{${k}}}`).join(", ");
734
+ const namespaces = [...new Set(undeclared.map((k) => k.split(".")[0] ?? k))].join(", ");
735
+ report({
736
+ message: `"${node.label ?? node.id}" contains literal template string(s) ${keyList} \u2014 destructure the proxy in .steps(({ ${namespaces} }) => \u2026) and pass the proxy value (e.g. \`${undeclared[0] ?? ""}\`) directly. Writing the template as a string bypasses type-checking and silently accepts typos.`,
737
+ rule: "no-literal-template-strings",
738
+ step: node.label ?? node.id
739
+ });
740
+ });
741
+ }
719
742
  var RULES = [
720
743
  exactTextMatch,
721
744
  noHardcodedData,
745
+ noLiteralTemplateStrings,
722
746
  preferPreconditionData,
723
747
  missingLabel,
724
748
  noDuplicateLabels,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ripplo/testing",
3
3
  "description": "TypeScript DSL for defining and running Ripplo e2e workflow tests",
4
- "version": "0.3.2",
4
+ "version": "0.3.3",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -1,20 +0,0 @@
1
- import {
2
- readLocator
3
- } from "./chunk-DCJBLS2U.js";
4
-
5
- // src/steps/to-spec-locator.ts
6
- function toSpecLocator(loc) {
7
- const spec = readLocator(loc);
8
- switch (spec.by) {
9
- case "role": {
10
- return { by: "role", name: spec.name, role: spec.role };
11
- }
12
- case "testId": {
13
- return { by: "testId", value: spec.value };
14
- }
15
- }
16
- }
17
-
18
- export {
19
- toSpecLocator
20
- };