@ripplo/testing 0.4.7 → 0.4.8

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
@@ -158,6 +158,7 @@ import {
158
158
  navigate,
159
159
  scrollIntoView,
160
160
  drag,
161
+ fixture,
161
162
  upload,
162
163
  handleDialog,
163
164
  clipboard,
@@ -171,10 +172,22 @@ fill(role("textbox", "Email"), "test@x.com"); // clear + type
171
172
  select(role("combobox", "Role"), "admin");
172
173
  check(role("checkbox", "Terms"));
173
174
  press("Enter");
174
- upload(testId("file-input"), "./test.png");
175
+ upload(testId("file-input"), fixture("logo.png"));
175
176
  drag(role("row", "Item 1"), role("row", "Item 2"));
176
177
  ```
177
178
 
179
+ ### Upload fixtures
180
+
181
+ `upload()` requires a `fixture()` reference. Fixture files live in `.ripplo/fixtures/` (committed). Limits: 10 MB per file, 50 MB total.
182
+
183
+ ```ts
184
+ // .ripplo/fixtures/logo.png exists
185
+ upload(testId("logo-input"), fixture("logo.png"));
186
+
187
+ // Multiple files
188
+ upload(testId("attachments"), [fixture("a.pdf"), fixture("b.pdf")]);
189
+ ```
190
+
178
191
  ### Assertions
179
192
 
180
193
  ```typescript
package/dist/actions.d.ts CHANGED
@@ -3,6 +3,14 @@ import { U as UnlabeledStep } from './step-De52hTLd.js';
3
3
  import { CheckLocator, InputLocator, AnyLocator, SelectLocator } from './locators.js';
4
4
  import '@ripplo/spec';
5
5
 
6
+ declare class Fixture {
7
+ readonly kind: "fixture";
8
+ readonly name: string;
9
+ constructor(name: string);
10
+ }
11
+ declare function fixture(name: string): Fixture;
12
+ declare function isFixture(value: unknown): value is Fixture;
13
+
6
14
  type StringOrVariable = string | Variable<string>;
7
15
  declare function navigate(url: string): UnlabeledStep<{
8
16
  type: "goto";
@@ -87,7 +95,7 @@ declare function press(key: string): UnlabeledStep<{
87
95
  key: string;
88
96
  type: "press";
89
97
  }>;
90
- declare function upload(locator: AnyLocator, path: string, options?: StepOptions): UnlabeledStep<{
98
+ declare function upload(locator: AnyLocator, files: Fixture | ReadonlyArray<Fixture>, options?: StepOptions): UnlabeledStep<{
91
99
  files: string[];
92
100
  locator: {
93
101
  by: "testId";
@@ -247,4 +255,4 @@ declare function setViewport({ height, width }: SetViewportOptions): UnlabeledSt
247
255
  width: number;
248
256
  }>;
249
257
 
250
- export { check, clear, click, clipboard, dblclick, drag, fill, focus, handleDialog, hover, navigate, press, rightClick, scrollIntoView, select, setPermission, setViewport, typeText, uncheck, upload };
258
+ export { Fixture, check, clear, click, clipboard, dblclick, drag, fill, fixture, focus, handleDialog, hover, isFixture, navigate, press, rightClick, scrollIntoView, select, setPermission, setViewport, typeText, uncheck, upload };
package/dist/actions.js CHANGED
@@ -9,6 +9,24 @@ import {
9
9
  } from "./chunk-MGATMMCZ.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
+ // src/steps/fixture.ts
13
+ var Fixture = class {
14
+ kind = "fixture";
15
+ name;
16
+ constructor(name) {
17
+ if (name.length === 0) {
18
+ throw new Error("fixture(name) requires a non-empty name");
19
+ }
20
+ this.name = name;
21
+ }
22
+ };
23
+ function fixture(name) {
24
+ return new Fixture(name);
25
+ }
26
+ function isFixture(value) {
27
+ return value instanceof Fixture;
28
+ }
29
+
12
30
  // src/steps/actions.ts
13
31
  function navigate(url) {
14
32
  return createStep({ type: "goto", url: { type: "static", value: url } });
@@ -46,9 +64,10 @@ function hover(locator) {
46
64
  function press(key) {
47
65
  return createStep({ key, type: "press" });
48
66
  }
49
- function upload(locator, path, options) {
67
+ function upload(locator, files, options) {
68
+ const list = files instanceof Fixture ? [files] : [...files];
50
69
  return createStep({
51
- files: [path],
70
+ files: list.map((f) => f.name),
52
71
  locator: toSpecLocator(locator),
53
72
  type: "upload",
54
73
  uiOnly: options?.uiOnly
@@ -101,6 +120,7 @@ function setViewport({ height, width }) {
101
120
  return createStep({ height, type: "setViewport", width });
102
121
  }
103
122
  export {
123
+ Fixture,
104
124
  check,
105
125
  clear,
106
126
  click,
@@ -108,9 +128,11 @@ export {
108
128
  dblclick,
109
129
  drag,
110
130
  fill,
131
+ fixture,
111
132
  focus,
112
133
  handleDialog,
113
134
  hover,
135
+ isFixture,
114
136
  navigate,
115
137
  press,
116
138
  rightClick,
@@ -25,7 +25,7 @@ function compile(ripplo) {
25
25
  };
26
26
  });
27
27
  const tests = testDefs.map((def) => compileTest(def, preconditionDefs));
28
- return { observers, preconditions, tests };
28
+ return { fixtures: {}, observers, preconditions, tests };
29
29
  }
30
30
  function validateUniqueIds(defs) {
31
31
  const seen = /* @__PURE__ */ new Map();
@@ -3,7 +3,12 @@ import { d as RipploBuilder } from './builder-BMjy83Iy.js';
3
3
  import './types-16SB7zjP.js';
4
4
  import './step-De52hTLd.js';
5
5
 
6
+ interface CompiledFixture {
7
+ readonly sha256: string;
8
+ readonly size: number;
9
+ }
6
10
  interface CompileResult {
11
+ readonly fixtures: Record<string, CompiledFixture>;
7
12
  readonly observers: Record<string, Observer>;
8
13
  readonly preconditions: Record<string, Precondition>;
9
14
  readonly tests: ReadonlyArray<CompiledTest>;
@@ -24,4 +29,4 @@ interface CompiledTest {
24
29
  }
25
30
  declare function compile(ripplo: RipploBuilder): CompileResult;
26
31
 
27
- export { type CompileResult, type CompiledTest, compile };
32
+ export { type CompileResult, type CompiledFixture, type CompiledTest, compile };
package/dist/compiler.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  compile
3
- } from "./chunk-DL3HLCD7.js";
3
+ } from "./chunk-YFOTJIVF.js";
4
4
  import "./chunk-MGATMMCZ.js";
5
5
  import "./chunk-4MGIQFAJ.js";
6
6
  export {
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-YQAEOH5W.js";
15
15
  import {
16
16
  compile
17
- } from "./chunk-DL3HLCD7.js";
17
+ } from "./chunk-YFOTJIVF.js";
18
18
  import "./chunk-MGATMMCZ.js";
19
19
  import {
20
20
  buildSetCookieHeader,
@@ -780,6 +780,30 @@ function observerParamsReferenceVariables(nodes, test2, report) {
780
780
  });
781
781
  });
782
782
  }
783
+ function uploadFixtureName(nodes, _test, report) {
784
+ nodes.forEach((node) => {
785
+ if (node.type !== "upload") {
786
+ return;
787
+ }
788
+ node.files.forEach((name) => {
789
+ if (name.length === 0) {
790
+ report({
791
+ message: `upload "${node.label ?? node.id}" references an empty fixture name`,
792
+ rule: "upload-fixture-name",
793
+ step: node.label ?? node.id
794
+ });
795
+ return;
796
+ }
797
+ if (name.includes("..") || name.startsWith("/")) {
798
+ report({
799
+ message: `upload "${node.label ?? node.id}" references "${name}" \u2014 fixture names must be relative paths under .ripplo/fixtures/, no ".." or absolute paths`,
800
+ rule: "upload-fixture-name",
801
+ step: node.label ?? node.id
802
+ });
803
+ }
804
+ });
805
+ });
806
+ }
783
807
  function noLiteralTemplateStrings(nodes, test2, report) {
784
808
  const declaredKeys = new Set(Object.keys(test2.spec.variables ?? {}));
785
809
  const pattern = /\{\{([^{}]+?)\}\}/g;
@@ -818,7 +842,8 @@ var RULES = [
818
842
  tautologicalPostClickAssert,
819
843
  expectedOutcomeKeywordCoverage,
820
844
  mutationWithoutObserverCoverage,
821
- observerParamsReferenceVariables
845
+ observerParamsReferenceVariables,
846
+ uploadFixtureName
822
847
  ];
823
848
 
824
849
  // src/engine.ts
@@ -6,7 +6,17 @@ import './types-16SB7zjP.js';
6
6
  import './step-De52hTLd.js';
7
7
 
8
8
  declare const LOCKFILE_RELATIVE_PATH = ".ripplo/ripplo.lock";
9
- declare const lockfileBodySchema: z.ZodObject<{
9
+ declare const FIXTURES_RELATIVE_PATH = ".ripplo/fixtures";
10
+ declare const fixtureEntrySchema: z.ZodObject<{
11
+ sha256: z.ZodString;
12
+ size: z.ZodNumber;
13
+ }, z.core.$strip>;
14
+ type FixtureEntry = z.infer<typeof fixtureEntrySchema>;
15
+ declare const lockfileBodyV2Schema: z.ZodObject<{
16
+ fixtures: z.ZodRecord<z.ZodString, z.ZodObject<{
17
+ sha256: z.ZodString;
18
+ size: z.ZodNumber;
19
+ }, z.core.$strip>>;
10
20
  observers: z.ZodRecord<z.ZodString, z.ZodObject<{
11
21
  budget: z.ZodEnum<{
12
22
  fast: "fast";
@@ -677,7 +687,7 @@ declare const lockfileBodySchema: z.ZodObject<{
677
687
  }, z.core.$strip>;
678
688
  }, z.core.$strip>>;
679
689
  }, z.core.$strip>;
680
- type Lockfile = z.infer<typeof lockfileBodySchema>;
690
+ type Lockfile = z.infer<typeof lockfileBodyV2Schema>;
681
691
  declare const lockfileCodec: Codec<Lockfile>;
682
692
  declare function compileResultToLockfile(result: CompileResult): Lockfile;
683
693
  declare function serializeLockfile(lockfile: Lockfile): string;
@@ -690,6 +700,11 @@ interface WriteLockfileParams {
690
700
  readonly result: CompileResult;
691
701
  }
692
702
  declare function writeLockfile({ cwd, result }: WriteLockfileParams): Promise<void>;
703
+ interface HashFixturesParams {
704
+ readonly cwd: string;
705
+ readonly result: CompileResult;
706
+ }
707
+ declare function hashFixturesIntoCompileResult({ cwd, result, }: HashFixturesParams): Promise<CompileResult>;
693
708
  type LockfileComparison = "match" | "missing" | "stale";
694
709
  interface CompareLockfileParams {
695
710
  readonly compiled: CompileResult;
@@ -697,4 +712,4 @@ interface CompareLockfileParams {
697
712
  }
698
713
  declare function compareCompileToLockfile({ compiled, existing, }: CompareLockfileParams): LockfileComparison;
699
714
 
700
- export { type CompareLockfileParams, LOCKFILE_RELATIVE_PATH, type Lockfile, type LockfileComparison, type ReadLockfileParams, type WriteLockfileParams, compareCompileToLockfile, compileResultToLockfile, lockfileCodec, readLockfile, serializeLockfile, writeLockfile };
715
+ export { type CompareLockfileParams, FIXTURES_RELATIVE_PATH, type FixtureEntry, LOCKFILE_RELATIVE_PATH, type Lockfile, type LockfileComparison, type ReadLockfileParams, type WriteLockfileParams, compareCompileToLockfile, compileResultToLockfile, hashFixturesIntoCompileResult, lockfileCodec, readLockfile, serializeLockfile, writeLockfile };
package/dist/lockfile.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import "./chunk-4MGIQFAJ.js";
2
2
 
3
3
  // src/lockfile.ts
4
+ import { createHash } from "crypto";
4
5
  import fs from "fs/promises";
5
6
  import path from "path";
6
7
 
@@ -500,6 +501,7 @@ var observerSchema = z9.object({
500
501
  // src/lockfile.ts
501
502
  import { z as z10 } from "zod";
502
503
  var LOCKFILE_RELATIVE_PATH = ".ripplo/ripplo.lock";
504
+ var FIXTURES_RELATIVE_PATH = ".ripplo/fixtures";
503
505
  var MAX_TESTS = 5e3;
504
506
  var requiresKeysSchema = z10.record(z10.string().max(200), z10.string().max(200));
505
507
  var compiledTestSchema = z10.object({
@@ -512,14 +514,32 @@ var compiledTestSchema = z10.object({
512
514
  sourcePath: z10.string().max(500).optional(),
513
515
  spec: workflowSpecSchema
514
516
  });
515
- var lockfileBodySchema = z10.object({
517
+ var fixtureEntrySchema = z10.object({
518
+ sha256: z10.string().regex(/^[0-9a-f]{64}$/u),
519
+ size: z10.number().int().nonnegative()
520
+ });
521
+ var fixturesMapSchema = z10.record(z10.string().min(1).max(500), fixtureEntrySchema);
522
+ var lockfileBodyV1Schema = z10.object({
523
+ observers: z10.record(z10.string().max(200), observerSchema),
524
+ preconditions: z10.record(z10.string().max(200), preconditionSchema),
525
+ tests: z10.array(compiledTestSchema).max(MAX_TESTS)
526
+ });
527
+ var lockfileBodyV2Schema = z10.object({
528
+ fixtures: fixturesMapSchema,
516
529
  observers: z10.record(z10.string().max(200), observerSchema),
517
530
  preconditions: z10.record(z10.string().max(200), preconditionSchema),
518
531
  tests: z10.array(compiledTestSchema).max(MAX_TESTS)
519
532
  });
520
- var lockfileCodec = defineCodec("ripplo-lockfile").initial(lockfileBodySchema).build();
533
+ var lockfileCodec = defineCodec("ripplo-lockfile").initial(lockfileBodyV1Schema).upgrade(
534
+ lockfileBodyV2Schema,
535
+ (prev) => ({
536
+ ...prev,
537
+ fixtures: {}
538
+ })
539
+ ).build();
521
540
  function compileResultToLockfile(result) {
522
541
  return {
542
+ fixtures: { ...result.fixtures },
523
543
  observers: result.observers,
524
544
  preconditions: result.preconditions,
525
545
  tests: result.tests.filter((test) => test.implemented).map((test) => ({
@@ -547,12 +567,76 @@ async function readLockfile({ cwd }) {
547
567
  }
548
568
  return decodeJson(lockfileCodec, raw);
549
569
  }
570
+ var MAX_FIXTURE_BYTES = 10 * 1024 * 1024;
571
+ var MAX_TOTAL_FIXTURE_BYTES = 50 * 1024 * 1024;
550
572
  async function writeLockfile({ cwd, result }) {
551
- const lockfile = compileResultToLockfile(result);
573
+ const hydrated = await hashFixturesIntoCompileResult({ cwd, result });
574
+ const lockfile = compileResultToLockfile(hydrated);
552
575
  const lockfilePath = path.join(cwd, LOCKFILE_RELATIVE_PATH);
553
576
  await fs.mkdir(path.dirname(lockfilePath), { recursive: true });
554
577
  await fs.writeFile(lockfilePath, serializeLockfile(lockfile), "utf8");
555
578
  }
579
+ async function hashFixturesIntoCompileResult({
580
+ cwd,
581
+ result
582
+ }) {
583
+ const referenced = collectFixtureReferences(result);
584
+ if (referenced.size === 0) {
585
+ return { ...result, fixtures: {} };
586
+ }
587
+ const fixturesRoot = path.join(cwd, FIXTURES_RELATIVE_PATH);
588
+ const sortedNames = [...referenced].toSorted((a, b) => a.localeCompare(b));
589
+ const hashed = await Promise.all(
590
+ sortedNames.map(async (name) => {
591
+ const entry = await hashOneFixture({ fixturesRoot, name });
592
+ if (entry.size > MAX_FIXTURE_BYTES) {
593
+ throw new Error(
594
+ `Fixture "${name}" is ${String(entry.size)} bytes; exceeds per-file limit of ${String(MAX_FIXTURE_BYTES)} bytes`
595
+ );
596
+ }
597
+ return [name, entry];
598
+ })
599
+ );
600
+ const total = hashed.reduce((sum, [, entry]) => sum + entry.size, 0);
601
+ if (total > MAX_TOTAL_FIXTURE_BYTES) {
602
+ throw new Error(
603
+ `Total fixtures size exceeds limit of ${String(MAX_TOTAL_FIXTURE_BYTES)} bytes`
604
+ );
605
+ }
606
+ return { ...result, fixtures: Object.fromEntries(hashed) };
607
+ }
608
+ async function hashOneFixture({ fixturesRoot, name }) {
609
+ if (name.includes("..") || path.isAbsolute(name)) {
610
+ throw new Error(`Invalid fixture name "${name}": must be a path under .ripplo/fixtures/`);
611
+ }
612
+ const abs = path.join(fixturesRoot, name);
613
+ const stat = await fs.lstat(abs).catch((error) => {
614
+ if (isNodeError(error) && error.code === "ENOENT") {
615
+ throw new Error(`Fixture "${name}" not found at ${abs}`);
616
+ }
617
+ throw error;
618
+ });
619
+ if (stat.isSymbolicLink()) {
620
+ throw new Error(`Fixture "${name}" is a symlink; symlinks are not allowed`);
621
+ }
622
+ if (!stat.isFile()) {
623
+ throw new Error(`Fixture "${name}" is not a regular file`);
624
+ }
625
+ const bytes = await fs.readFile(abs);
626
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
627
+ return { sha256, size: bytes.byteLength };
628
+ }
629
+ function collectFixtureReferences(result) {
630
+ const names = /* @__PURE__ */ new Set();
631
+ result.tests.forEach((test) => {
632
+ Object.values(test.spec.nodes).forEach((node) => {
633
+ if (node.type === "upload") {
634
+ node.files.forEach((name) => names.add(name));
635
+ }
636
+ });
637
+ });
638
+ return names;
639
+ }
556
640
  function compareCompileToLockfile({
557
641
  compiled,
558
642
  existing
@@ -589,9 +673,11 @@ function isPlainObject(value) {
589
673
  return typeof value === "object" && value !== null && !Array.isArray(value);
590
674
  }
591
675
  export {
676
+ FIXTURES_RELATIVE_PATH,
592
677
  LOCKFILE_RELATIVE_PATH,
593
678
  compareCompileToLockfile,
594
679
  compileResultToLockfile,
680
+ hashFixturesIntoCompileResult,
595
681
  lockfileCodec,
596
682
  readLockfile,
597
683
  serializeLockfile,
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.4.7",
4
+ "version": "0.4.8",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -99,8 +99,8 @@
99
99
  "tsup": "^8.5.1",
100
100
  "typescript": "catalog:",
101
101
  "vitest": "^4.1.4",
102
- "@ripplo/spec": "^0.0.0",
103
- "@ripplo/eslint-config": "0.0.0"
102
+ "@ripplo/eslint-config": "0.0.0",
103
+ "@ripplo/spec": "^0.0.0"
104
104
  },
105
105
  "peerDependencies": {
106
106
  "@nestjs/common": "^10.0.0 || ^11.0.0",