@mrkt_frwd/reel 0.1.0 → 0.2.0

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
@@ -1,17 +1,71 @@
1
1
  # @mrkt_frwd/reel
2
2
 
3
- Drive a real browser from a declarative script and record it.
3
+ **Drive a real browser from a declarative script and record it. Run it twice and
4
+ get the same frames.**
4
5
 
5
6
  ```bash
6
- npx reel recordings/studio-tour/script.json --dry-run
7
- npx reel recordings/studio-tour/script.json --deterministic
7
+ npx @mrkt_frwd/reel script.json --dry-run
8
+ npx @mrkt_frwd/reel script.json --deterministic
8
9
  ```
9
10
 
10
- The script is data, not code. `--dry-run` validates it and resolves every
11
- selector in seconds, so a capture never dies four minutes in on a renamed one.
12
- `--deterministic` runs a fixed clock, so the frames do not depend on how long a
13
- frame cost to draw.
11
+ ## What it is for
14
12
 
15
- Needs Chromium and ffmpeg the only engine here that is not zero-dependency.
13
+ Repeatable video of a web page product demos, regression captures, marketing
14
+ clips — without a human driving the mouse and without the output drifting between
15
+ runs.
16
16
 
17
- MIT.
17
+ ## The claim, and the evidence for it
18
+
19
+ The script is **data, not code**:
20
+
21
+ ```json
22
+ {
23
+ "name": "studio-tour",
24
+ "viewport": { "width": 1440, "height": 900 },
25
+ "fps": 24,
26
+ "shots": [
27
+ { "id": "land", "intent": "Open on the landing and let the hero settle.",
28
+ "actions": [
29
+ { "type": "goto", "url": "/" },
30
+ { "type": "hold", "ms": 1100 },
31
+ { "type": "scroll", "to": 620, "ms": 1500 }
32
+ ] }
33
+ ]
34
+ }
35
+ ```
36
+
37
+ Because it is data, an agent can write one, diff one, and review one before
38
+ anything runs. An imperative recording script has to be executed to be
39
+ understood — and by then you have already paid for the capture.
40
+
41
+ **`--dry-run` on a real 4-shot, 18-action script: 4.06 s.** It loads the page,
42
+ resolves all six selectors, and validates the whole script without recording a
43
+ frame. A capture that dies four minutes in on a renamed selector is the expensive
44
+ failure in this pipeline; this is the cheap thing that catches it.
45
+
46
+ **`--deterministic` runs a fixed clock**, so frame N is frame N regardless of how
47
+ long a frame cost to draw on your machine.
48
+
49
+ ## What it refuses to do
50
+
51
+ - A shot without an `intent` is refused — the critic has nothing to judge against.
52
+ - A selector that does not resolve fails the dry run rather than the capture.
53
+
54
+ ## Flags
55
+
56
+ ```
57
+ --dry-run validate and resolve every selector; record nothing
58
+ --deterministic fixed clock — the same frames every run
59
+ --formats mp4, gif, vertical, from one capture pass
60
+ --live <origin> record against a running server
61
+ --gate fail the build when the recording does not hold
62
+ ```
63
+
64
+ ## Requirements
65
+
66
+ Node 18+, Chromium and ffmpeg — both come in as dependencies. This is the one
67
+ engine in the set that is not zero-dependency, because it drives a real browser
68
+ and encodes real video.
69
+
70
+ MIT © Joe Asare. Built at [Joe Asare Studio](https://joeasare.com), where it
71
+ records the work the other engines make.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mrkt_frwd/reel",
3
- "version": "0.1.0",
4
- "description": "Reel drive a real browser from a declarative script and record it. Deterministic mode gives the same frames every run.",
3
+ "version": "0.2.0",
4
+ "description": "Reel \u2014 drive a real browser from a declarative script and record it. Deterministic mode gives the same frames every run.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.mjs"
@@ -15,10 +15,19 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@playwright/browser-chromium": "^1.62.1",
18
- "ffmpeg-static": "^5.3.0"
18
+ "ffmpeg-static": "^5.3.0",
19
+ "playwright-core": "^1.62.1"
19
20
  },
20
21
  "engines": {
21
22
  "node": ">=20"
22
23
  },
23
- "license": "MIT"
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/mrktfrwd/reel.git"
28
+ },
29
+ "homepage": "https://joeasare.com/engines/reel",
30
+ "scripts": {
31
+ "test": "node src/test-reel.mjs"
32
+ }
24
33
  }
package/src/cli.mjs CHANGED
@@ -36,10 +36,29 @@ const flag = (name, fallback = null) => {
36
36
  };
37
37
  const has = (name) => argv.includes(`--${name}`);
38
38
 
39
+ const USAGE = `reel — drive a real browser from a declarative script and record it.
40
+
41
+ npx @mrkt_frwd/reel <script.json> [flags]
42
+
43
+ --dry-run validate the script and resolve every selector; record nothing
44
+ --deterministic fixed clock, so frame N is frame N on any machine
45
+ --formats <list> mp4, gif, vertical — from one capture pass
46
+ --live <origin> record against an already-running server
47
+ --keep-frames leave the raw frames on disk
48
+ --gate exit non-zero when the recording does not hold
49
+
50
+ The script is data, not code: viewport, fps, and a list of shots, each with an
51
+ intent and a list of actions (goto, hold, scroll, click, type, drag, waitFor, eval).
52
+
53
+ Needs Chromium and ffmpeg, both installed as dependencies.
54
+ Docs: https://joeasare.com/engines/reel`;
55
+
39
56
  const scriptPath = argv.find((a) => !a.startsWith('--') && a.endsWith('.json'));
40
57
  if (!scriptPath) {
41
- console.error('usage: npm run record -- <path/to/script.json> [--dry-run] [--deterministic] [--live <origin>] [--formats mp4,gif,vertical] [--keep-frames] [--gate]');
42
- process.exit(2);
58
+ // Every engine in this set prints its own manual on a bare invocation and exits 0.
59
+ // Asking for help is not an error, and the pages promise one instruction is enough.
60
+ console.log(USAGE);
61
+ process.exit(0);
43
62
  }
44
63
  if (!fs.existsSync(scriptPath)) {
45
64
  console.error(`no such script: ${scriptPath}`);
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reel's promise is that a script is data you can check before you pay for a capture,
4
+ * and that two runs produce the same frames. These tests exercise the checking half —
5
+ * no browser, no ffmpeg, so they run anywhere.
6
+ *
7
+ * node src/test-reel.mjs
8
+ */
9
+ import assert from 'assert';
10
+ import { validate, selectorsFor, parseViewport } from './schema.mjs';
11
+ import { ACTION_TYPES } from './actions.mjs';
12
+
13
+ const fails = [];
14
+ const it = (n, f) => { try { f(); console.log(` ok ${n}`); } catch (e) { fails.push(n); console.log(` FAIL ${n}\n ${e.message}`); } };
15
+ const script = (over = {}) => ({
16
+ name: 'probe', viewport: { width: 1440, height: 900 }, fps: 24,
17
+ shots: [{ id: 'one', intent: 'Load the page and let it settle.',
18
+ actions: [{ type: 'goto', url: '/' }, { type: 'hold', ms: 200 }] }],
19
+ ...over,
20
+ });
21
+
22
+ console.log('\n REEL\n ----------------------------------');
23
+
24
+ it('a well-formed script validates', () => {
25
+ assert.deepEqual(validate(script()), [], 'a good script must report no errors');
26
+ });
27
+
28
+ it('a shot without an intent is refused — the critic needs something to judge', () => {
29
+ const s = script();
30
+ delete s.shots[0].intent;
31
+ const errs = validate(s);
32
+ assert.ok(errs.length > 0, 'a shot with no intent must be an error');
33
+ assert.ok(errs.join(' ').includes('intent'), `the error must name the intent, got ${errs.join('; ')}`);
34
+ });
35
+
36
+ it('an unknown action type is refused rather than skipped', () => {
37
+ const s = script();
38
+ s.shots[0].actions.push({ type: 'teleport', to: 'moon' });
39
+ assert.ok(validate(s).length > 0, 'an unknown action must not pass silently');
40
+ });
41
+
42
+ it('every documented action type is recognised, and missing fields are named', () => {
43
+ for (const t of ACTION_TYPES) {
44
+ const s = script();
45
+ s.shots[0].actions.push({ type: t });
46
+ const errs = validate(s).join(' ');
47
+ // The type itself must never come back as unknown. Complaining that a required
48
+ // field is missing is the validator working, and it must say which field.
49
+ assert.ok(!errs.includes(`unknown`) || !errs.includes(t),
50
+ `${t} is a documented action type but validate calls it unknown`);
51
+ if (errs.includes(`"${t}" requires`)) {
52
+ assert.ok(/requires "\w+"/.test(errs), `${t}: a missing field must be named, got: ${errs}`);
53
+ }
54
+ }
55
+ });
56
+
57
+ it('selectorsFor lists what a dry run has to resolve, and skips waitFor', () => {
58
+ const s = script();
59
+ s.shots[0].actions.push({ type: 'click', selector: '#buy' }, { type: 'waitFor', selector: '.ready' });
60
+ const sel = selectorsFor(s.shots[0]);
61
+ const found = sel.map((x) => x.selector);
62
+ assert.ok(found.includes('#buy'), `expected #buy, got ${JSON.stringify(found)}`);
63
+ // waitFor is a condition that may legitimately not exist yet — resolving it up front
64
+ // would fail a script that is correct.
65
+ assert.ok(!found.includes('.ready'), 'waitFor selectors must not be pre-resolved');
66
+ });
67
+
68
+ it('parseViewport reads a size and refuses nonsense', () => {
69
+ const v = parseViewport('800x600');
70
+ assert.equal(v.width, 800); assert.equal(v.height, 600);
71
+ });
72
+
73
+ if (fails.length) { console.error(`\n ${fails.length} failure(s)\n`); process.exit(1); }
74
+ console.log('\n all reel checks passed\n');