lynkeus 0.2.0 → 0.3.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/lib/cases/ahead.d.ts +31 -0
- package/lib/cases/ahead.js +90 -0
- package/lib/cases/fixtures.d.ts +12 -0
- package/lib/cases/fixtures.js +32 -0
- package/lib/cases/flakes.d.ts +28 -0
- package/lib/cases/flakes.js +61 -0
- package/lib/cases/format.d.ts +5 -0
- package/lib/cases/format.js +30 -4
- package/lib/cases/portClaim.d.ts +10 -0
- package/lib/cases/portClaim.js +55 -0
- package/lib/cases/runfile.d.ts +24 -0
- package/lib/cases/runfile.js +108 -0
- package/lib/cases/runner.d.ts +80 -3
- package/lib/cases/runner.js +387 -40
- package/lib/cases/templates.d.ts +2 -2
- package/lib/cases/templates.js +14 -4
- package/lib/cases/traces.d.ts +58 -0
- package/lib/cases/traces.js +250 -0
- package/lib/cli.js +27 -6
- package/lib/commands/cases.js +360 -37
- package/lib/commands/check.d.ts +6 -0
- package/lib/commands/check.js +8 -3
- package/lib/commands/drive.js +32 -8
- package/lib/commands/headless.js +12 -2
- package/lib/commands/init.d.ts +6 -0
- package/lib/commands/init.js +78 -21
- package/lib/commands/report.d.ts +97 -0
- package/lib/commands/report.js +228 -0
- package/lib/config.d.ts +10 -1
- package/lib/device/agentVersion.d.ts +7 -0
- package/lib/device/agentVersion.js +12 -0
- package/lib/device/devices.d.ts +5 -0
- package/lib/device/devices.js +27 -0
- package/lib/flow/runner.js +33 -1
- package/lib/flow/types.d.ts +2 -1
- package/lib/knowledge/contrast.d.ts +45 -0
- package/lib/knowledge/contrast.js +114 -0
- package/lib/knowledge/errors.js +1 -1
- package/lib/knowledge/lint.d.ts +2 -0
- package/lib/knowledge/lint.js +13 -3
- package/lib/registry.d.ts +5 -0
- package/lib/targets.d.ts +10 -1
- package/lib/targets.js +16 -3
- package/package.json +10 -6
- package/src/cases/ahead.ts +111 -0
- package/src/cases/fixtures.ts +37 -0
- package/src/cases/flakes.ts +67 -0
- package/src/cases/format.ts +31 -6
- package/src/cases/portClaim.ts +52 -0
- package/src/cases/runfile.ts +119 -0
- package/src/cases/runner.ts +446 -46
- package/src/cases/templates.ts +14 -4
- package/src/cases/traces.ts +298 -0
- package/src/cli.ts +28 -6
- package/src/commands/cases.ts +375 -37
- package/src/commands/check.ts +11 -4
- package/src/commands/drive.ts +31 -8
- package/src/commands/headless.ts +12 -2
- package/src/commands/init.ts +84 -20
- package/src/commands/report.ts +272 -0
- package/src/config.ts +6 -2
- package/src/device/agentVersion.ts +15 -0
- package/src/device/devices.ts +28 -0
- package/src/flow/runner.ts +35 -1
- package/src/flow/types.ts +2 -2
- package/src/knowledge/contrast.ts +135 -0
- package/src/knowledge/errors.ts +1 -1
- package/src/knowledge/lint.ts +15 -4
- package/src/registry.ts +2 -0
- package/src/targets.ts +17 -3
- package/lib/__tests__/args.test.d.ts +0 -1
- package/lib/__tests__/args.test.js +0 -69
- package/lib/__tests__/check.test.d.ts +0 -1
- package/lib/__tests__/check.test.js +0 -60
- package/lib/__tests__/init.test.d.ts +0 -1
- package/lib/__tests__/init.test.js +0 -42
- package/lib/__tests__/record.test.d.ts +0 -1
- package/lib/__tests__/record.test.js +0 -44
- package/lib/__tests__/snapshot.test.d.ts +0 -1
- package/lib/__tests__/snapshot.test.js +0 -47
- package/lib/__tests__/targets.test.d.ts +0 -1
- package/lib/__tests__/targets.test.js +0 -127
- package/lib/cases/__tests__/format.test.d.ts +0 -1
- package/lib/cases/__tests__/format.test.js +0 -61
- package/lib/cases/__tests__/runner.test.d.ts +0 -1
- package/lib/cases/__tests__/runner.test.js +0 -188
- package/lib/knowledge/__tests__/edges.test.d.ts +0 -1
- package/lib/knowledge/__tests__/edges.test.js +0 -25
- package/lib/knowledge/__tests__/events.test.d.ts +0 -1
- package/lib/knowledge/__tests__/events.test.js +0 -39
- package/lib/knowledge/__tests__/extract.test.d.ts +0 -1
- package/lib/knowledge/__tests__/extract.test.js +0 -69
- package/lib/knowledge/__tests__/frontier.test.d.ts +0 -1
- package/lib/knowledge/__tests__/frontier.test.js +0 -57
- package/lib/knowledge/__tests__/funnels.test.d.ts +0 -1
- package/lib/knowledge/__tests__/funnels.test.js +0 -40
- package/lib/knowledge/__tests__/lint.test.d.ts +0 -1
- package/lib/knowledge/__tests__/lint.test.js +0 -53
- package/lib/knowledge/__tests__/perf.test.d.ts +0 -1
- package/lib/knowledge/__tests__/perf.test.js +0 -34
- package/lib/knowledge/__tests__/plan.test.d.ts +0 -1
- package/lib/knowledge/__tests__/plan.test.js +0 -35
- package/lib/knowledge/__tests__/query.test.d.ts +0 -1
- package/lib/knowledge/__tests__/query.test.js +0 -55
- package/lib/knowledge/__tests__/screens.test.d.ts +0 -1
- package/lib/knowledge/__tests__/screens.test.js +0 -30
- package/lib/knowledge/__tests__/why.test.d.ts +0 -1
- package/lib/knowledge/__tests__/why.test.js +0 -28
- package/src/__tests__/args.test.ts +0 -82
- package/src/__tests__/check.test.ts +0 -71
- package/src/__tests__/init.test.ts +0 -52
- package/src/__tests__/record.test.ts +0 -49
- package/src/__tests__/snapshot.test.ts +0 -51
- package/src/__tests__/targets.test.ts +0 -132
- package/src/cases/__tests__/format.test.ts +0 -76
- package/src/cases/__tests__/runner.test.ts +0 -230
- package/src/knowledge/__tests__/edges.test.ts +0 -34
- package/src/knowledge/__tests__/events.test.ts +0 -45
- package/src/knowledge/__tests__/extract.test.ts +0 -88
- package/src/knowledge/__tests__/frontier.test.ts +0 -77
- package/src/knowledge/__tests__/funnels.test.ts +0 -58
- package/src/knowledge/__tests__/lint.test.ts +0 -66
- package/src/knowledge/__tests__/perf.test.ts +0 -41
- package/src/knowledge/__tests__/plan.test.ts +0 -52
- package/src/knowledge/__tests__/query.test.ts +0 -74
- package/src/knowledge/__tests__/screens.test.ts +0 -37
- package/src/knowledge/__tests__/why.test.ts +0 -40
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
import { diffGeometry, record } from '../screens.js';
|
|
7
|
-
const f = (x, y, w, h) => ({ x, y, w, h });
|
|
8
|
-
test('drift within the tolerance is not a change', () => {
|
|
9
|
-
assert.deepEqual(diffGeometry({ a: f(10, 10, 100, 40) }, { a: f(12, 8, 102, 41) }), []);
|
|
10
|
-
});
|
|
11
|
-
test('a move past the tolerance is reported with both positions', () => {
|
|
12
|
-
assert.deepEqual(diffGeometry({ a: f(10, 10, 100, 40) }, { a: f(10, 60, 100, 40) }), ['#a moved (10,10)→(10,60)']);
|
|
13
|
-
});
|
|
14
|
-
test('a resize is reported', () => {
|
|
15
|
-
assert.deepEqual(diffGeometry({ a: f(0, 0, 100, 40) }, { a: f(0, 0, 100, 80) }), ['#a resized 100×40→100×80']);
|
|
16
|
-
});
|
|
17
|
-
test('an element that collapsed to zero is named as such', () => {
|
|
18
|
-
assert.deepEqual(diffGeometry({ a: f(0, 0, 100, 40) }, { a: f(0, 0, 100, 0) }), ['#a collapsed to 100×0 (was 100×40)']);
|
|
19
|
-
});
|
|
20
|
-
test('elements only on one side are not geometry changes', () => {
|
|
21
|
-
assert.deepEqual(diffGeometry({ a: f(0, 0, 1, 1) }, { b: f(0, 0, 1, 1) }), []);
|
|
22
|
-
});
|
|
23
|
-
test('a recorded screen keeps what is presented, not what another tab or a covered screen holds', () => {
|
|
24
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-screens-'));
|
|
25
|
-
const el = (i, testId, hidden) => ({ i, kind: 'button', frame: f(0, i * 50, 100, 40), enabled: true, depth: 0, testId, hidden });
|
|
26
|
-
const screen = (elements) => ({ route: 'Settings.Main', path: ['Settings.Main'], commit: 1, busy: false, window: { w: 390, h: 844 }, costMs: 1, elements });
|
|
27
|
-
record(dir, screen([el(0, 'save'), el(1, 'other-tab', 'a11y'), el(2, 'slide', 'inert')]));
|
|
28
|
-
const change = record(dir, screen([el(0, 'save'), el(2, 'slide', 'inert')]));
|
|
29
|
-
assert.equal(change, null);
|
|
30
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
import { explain } from '../why.js';
|
|
4
|
-
const el = (p, i) => ({ i, kind: 'button', frame: { x: 0, y: 0, w: 10, h: 10 }, enabled: true, depth: 1, tag: i + 1, ...p });
|
|
5
|
-
const screen = (elements, route) => ({ route, path: route ? [route] : [], elements, commit: 5, costMs: 1 });
|
|
6
|
-
const req = (status, url, error) => ({ id: 1, method: 'GET', url, status, startedAt: 0, ms: 10, error });
|
|
7
|
-
const rules = (f) => f.map((x) => x.rule);
|
|
8
|
-
test('a dead navigator and a gone session are named', () => {
|
|
9
|
-
assert.deepEqual(rules(explain({ screen: screen([]), requests: [], inFlight: 0 })), ['dead-navigator']);
|
|
10
|
-
assert.ok(rules(explain({ screen: screen([el({}, 0)], 'Home'), requests: [req(401, 'http://x/a'), req(401, 'http://x/b')], inFlight: 0 })).includes('session-gone'));
|
|
11
|
-
});
|
|
12
|
-
test('a covered target, a near text and a capped list', () => {
|
|
13
|
-
const s = screen([el({ testId: 'submit', covered: true }, 0), el({ kind: 'text', text: 'Café Ünïcode wörks here' }, 1)], 'Home');
|
|
14
|
-
assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { testId: 'submit' } })).includes('covered'));
|
|
15
|
-
assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'Cafe Unicode works here' } })).includes('near-match'));
|
|
16
|
-
assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'cafe unicode works here' } })).includes('near-match'));
|
|
17
|
-
assert.ok(!rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'A completely different sentence' } })).includes('near-match'));
|
|
18
|
-
const list = screen(Array.from({ length: 12 }, (_, i) => el({ testId: `row-${i}-item`, text: `Item${i}` }, i)), 'Search');
|
|
19
|
-
assert.ok(rules(explain({ screen: list, requests: [req(200, 'http://x/search')], inFlight: 0, target: { text: 'itemabc' } })).includes('capped-list'));
|
|
20
|
-
});
|
|
21
|
-
test('backend and loading conditions', () => {
|
|
22
|
-
const s = screen([el({}, 0)], 'Home');
|
|
23
|
-
assert.ok(rules(explain({ screen: s, requests: [req(500, 'http://x/api/a')], inFlight: 0 })).includes('backend-error'));
|
|
24
|
-
assert.ok(rules(explain({ screen: s, requests: [req(200, 'http://x/a')], inFlight: 2, target: { testId: 'x' } })).includes('in-flight'));
|
|
25
|
-
const stall = { seq: 1, t: 1, kind: 'stall', renders: 2000, windowMs: 3000, component: 'Feed' };
|
|
26
|
-
assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, trace: [stall] })).includes('render-loop'));
|
|
27
|
-
assert.deepEqual(rules(explain({ screen: s, requests: [req(200, 'http://x/a')], inFlight: 0 })), []);
|
|
28
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { pairs, parseArgs, splitLine, UsageError } from '../args.js';
|
|
5
|
-
import { driveCommands } from '../commands/drive.js';
|
|
6
|
-
import { headlessCommands } from '../commands/headless.js';
|
|
7
|
-
import { mapCommands } from '../commands/map.js';
|
|
8
|
-
import { osCommands } from '../commands/os.js';
|
|
9
|
-
import { runsCommands } from '../commands/runs.js';
|
|
10
|
-
|
|
11
|
-
const FLAGS = {
|
|
12
|
-
direct: { type: 'boolean' as const, help: '' },
|
|
13
|
-
depth: { type: 'number' as const, help: '', default: 4 },
|
|
14
|
-
var: { type: 'list' as const, help: '' },
|
|
15
|
-
out: { type: 'string' as const, help: '' },
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
test('booleans never eat the next positional; numbers are numbers; lists collect', () => {
|
|
19
|
-
const p = parseArgs(
|
|
20
|
-
['--direct', 'a.ts', 'b.ts', '--depth', '2', '--var', 'x=1', '--var', 'y=2'],
|
|
21
|
-
[{ name: 'file', help: '', required: true, rest: true }],
|
|
22
|
-
FLAGS,
|
|
23
|
-
);
|
|
24
|
-
assert.deepEqual(p.args, ['a.ts', 'b.ts']);
|
|
25
|
-
assert.equal(p.flags.direct, true);
|
|
26
|
-
assert.equal(p.flags.depth, 2);
|
|
27
|
-
assert.deepEqual(p.flags.var, ['x=1', 'y=2']);
|
|
28
|
-
assert.deepEqual(pairs(p.flags.var as string[]), { x: '1', y: '2' });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('defaults, --no-flag, --flag=value and passthrough after --', () => {
|
|
32
|
-
const p = parseArgs(['--no-direct', '--out=x.json', '--', '--jest-flag'], [], FLAGS);
|
|
33
|
-
assert.equal(p.flags.direct, false);
|
|
34
|
-
assert.equal(p.flags.depth, 4);
|
|
35
|
-
assert.equal(p.flags.out, 'x.json');
|
|
36
|
-
assert.deepEqual(p.passthrough, ['--jest-flag']);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('unknown flags, missing values and missing positionals are usage errors that name what exists', () => {
|
|
40
|
-
assert.throws(
|
|
41
|
-
() => parseArgs(['--nope'], [], FLAGS),
|
|
42
|
-
(e: unknown) => e instanceof UsageError && /--direct/.test((e as Error).message),
|
|
43
|
-
);
|
|
44
|
-
assert.throws(() => parseArgs(['--depth', '--direct'], [], FLAGS), /--depth needs a value/);
|
|
45
|
-
assert.throws(() => parseArgs(['--depth', 'x'], [], FLAGS), /expects a number/);
|
|
46
|
-
assert.throws(() => parseArgs([], [{ name: 'target', help: '', required: true }], FLAGS), /missing <target>/);
|
|
47
|
-
assert.throws(() => parseArgs(['a', 'b'], [{ name: 'one', help: '' }], FLAGS), /unexpected argument "b"/);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('a session line splits on spaces and keeps quoted text together', () => {
|
|
51
|
-
assert.deepEqual(splitLine('press "Comenzar ya" --hold 200'), ['press', 'Comenzar ya', '--hold', '200']);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('every command is named once, documented, and its aliases are unique', () => {
|
|
55
|
-
const all = [...driveCommands, ...osCommands, ...mapCommands, ...runsCommands, ...headlessCommands];
|
|
56
|
-
const names = new Set<string>();
|
|
57
|
-
for (const c of all) {
|
|
58
|
-
assert.ok(c.summary.length > 10, `${c.name} has a summary`);
|
|
59
|
-
assert.ok(!names.has(c.name), `${c.name} defined once`);
|
|
60
|
-
names.add(c.name);
|
|
61
|
-
for (const a of c.aliases ?? []) {
|
|
62
|
-
assert.ok(!names.has(a), `${a} unique`);
|
|
63
|
-
names.add(a);
|
|
64
|
-
}
|
|
65
|
-
for (const [flag, spec] of Object.entries(c.flags ?? {})) assert.ok(spec.help, `${c.name} --${flag} has help`);
|
|
66
|
-
for (const p of c.positionals ?? []) assert.ok(p.help, `${c.name} <${p.name}> has help`);
|
|
67
|
-
}
|
|
68
|
-
for (const short of ['plan', 'deeplink', 'appstate', 'network', 'clock', 'biometrics', 'scan', 'gallery', 'headless'])
|
|
69
|
-
assert.ok(names.has(short), `${short} accepted`);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('MCP serves a small core by default and everything else on request', () => {
|
|
73
|
-
const all = [...driveCommands, ...osCommands, ...mapCommands, ...runsCommands, ...headlessCommands];
|
|
74
|
-
const core = all.filter((c) => c.mcp === 'core').map((c) => c.name);
|
|
75
|
-
assert.deepEqual(core.sort(), ['call', 'map plan', 'map show', 'os deeplink', 'press', 'screen', 'type', 'wait', 'why']);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('what MCP leaves out of a tool is still a flag of the command', () => {
|
|
79
|
-
for (const c of [...driveCommands, ...osCommands, ...mapCommands, ...runsCommands, ...headlessCommands]) {
|
|
80
|
-
for (const flag of c.mcpOmit ?? []) assert.ok(c.flags?.[flag], `${c.name} omits --${flag}, which it does not have`);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { diffGraphs, newFindings } from '../commands/check.js';
|
|
5
|
-
import { mapCheckWorkflow } from '../commands/map.js';
|
|
6
|
-
import { summarizeRuns } from '../commands/flake.js';
|
|
7
|
-
import type { FlowReport } from '../flow/types.js';
|
|
8
|
-
import type { ScreenGraph } from '../knowledge/types.js';
|
|
9
|
-
|
|
10
|
-
const graph = (screens: Record<string, string | null>, files: Record<string, { selectors?: string[]; navigates?: string[] }>): ScreenGraph =>
|
|
11
|
-
({
|
|
12
|
-
app: 'x',
|
|
13
|
-
root: '/x',
|
|
14
|
-
extractedAt: '',
|
|
15
|
-
warnings: [],
|
|
16
|
-
screens: Object.fromEntries(Object.entries(screens).map(([route, file]) => [route, { route, component: route, file, declaredIn: 'nav.tsx' }])),
|
|
17
|
-
files: Object.fromEntries(
|
|
18
|
-
Object.entries(files).map(([file, f]) => [
|
|
19
|
-
file,
|
|
20
|
-
{
|
|
21
|
-
selectors: (f.selectors ?? []).map((id) => ({ id, line: 1 })),
|
|
22
|
-
navigates: (f.navigates ?? []).map((to) => ({ to, line: 1, confidence: 'static', via: { kind: 'unknown' } })),
|
|
23
|
-
imports: [],
|
|
24
|
-
},
|
|
25
|
-
]),
|
|
26
|
-
),
|
|
27
|
-
}) as unknown as ScreenGraph;
|
|
28
|
-
|
|
29
|
-
test('a graph diff names routes, navigations and testIDs that changed', () => {
|
|
30
|
-
const base = graph({ A: 'a.tsx', B: 'b.tsx' }, { 'a.tsx': { selectors: ['go-b'], navigates: ['B'] }, 'b.tsx': { selectors: ['submit'] } });
|
|
31
|
-
const head = graph({ A: 'a.tsx', B: 'b.tsx', C: 'c.tsx' }, { 'a.tsx': { selectors: ['go-b', 'go-c'], navigates: ['B', 'C'] }, 'b.tsx': {}, 'c.tsx': {} });
|
|
32
|
-
const d = diffGraphs(base, head);
|
|
33
|
-
assert.deepEqual(d.routesAdded, ['C']);
|
|
34
|
-
assert.deepEqual(d.edgesAdded, [{ from: 'A', to: 'C' }]);
|
|
35
|
-
assert.deepEqual(d.selectorsAdded, [{ file: 'a.tsx', id: 'go-c' }]);
|
|
36
|
-
assert.deepEqual(d.selectorsRemoved, [{ file: 'b.tsx', id: 'submit' }]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('lint findings already in the baseline are not new', () => {
|
|
40
|
-
const f1 = { finding: { rule: 'unlabeled-control', severity: 'warn' as const, element: 3, message: 'button without a label' }, testId: 'x' };
|
|
41
|
-
const f2 = { finding: { rule: 'small-target', severity: 'warn' as const, element: 4, message: '20×20' }, testId: 'y' };
|
|
42
|
-
const fresh = newFindings({ A: [{ rule: 'unlabeled-control', testId: 'x', message: 'button without a label' }] }, 'A', [f1, f2]);
|
|
43
|
-
assert.deepEqual(fresh, [f2]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('flake runs are summarised by the step and error they died on', () => {
|
|
47
|
-
const ok = (ms: number): FlowReport => ({ name: 'f', ok: true, totalMs: ms, vars: {}, steps: [{ step: { press: { testId: 'a' } }, ok: true, ms: 1 }] });
|
|
48
|
-
const bad = (err: string): FlowReport => ({
|
|
49
|
-
name: 'f',
|
|
50
|
-
ok: false,
|
|
51
|
-
totalMs: 900,
|
|
52
|
-
vars: {},
|
|
53
|
-
steps: [
|
|
54
|
-
{ step: { press: { testId: 'a' } }, ok: true, ms: 1 },
|
|
55
|
-
{ step: { waitFor: { route: 'A' } }, ok: false, ms: 1, error: err },
|
|
56
|
-
],
|
|
57
|
-
});
|
|
58
|
-
const s = summarizeRuns([ok(100), bad('Timed out waiting for A (on Z)'), ok(300), bad('Timed out waiting for A (on Y)')]);
|
|
59
|
-
assert.equal(s.passed, 2);
|
|
60
|
-
assert.equal(s.failures.length, 1);
|
|
61
|
-
assert.equal(s.failures[0]!.times, 2);
|
|
62
|
-
assert.equal(s.failures[0]!.step, 2);
|
|
63
|
-
assert.match(s.failures[0]!.what, /waitFor/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('the map check workflow diffs against the pull request base and keeps one comment', () => {
|
|
67
|
-
const yml = mapCheckWorkflow(2);
|
|
68
|
-
assert.match(yml, /lynkeus map check --base origin\/\$\{\{ github\.base_ref \}\} --depth 2/);
|
|
69
|
-
assert.match(yml, /--edit-last/);
|
|
70
|
-
assert.match(yml, /pull-requests: write/);
|
|
71
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
|
|
7
|
-
import { init } from '../commands/init.js';
|
|
8
|
-
import { explainError } from '../knowledge/errors.js';
|
|
9
|
-
|
|
10
|
-
const tmpApp = () => {
|
|
11
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-init-'));
|
|
12
|
-
fs.mkdirSync(path.join(root, 'app'));
|
|
13
|
-
fs.writeFileSync(path.join(root, 'app', 'App.tsx'), 'export default () => null');
|
|
14
|
-
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'demo' }));
|
|
15
|
-
fs.writeFileSync(
|
|
16
|
-
path.join(root, 'index.js'),
|
|
17
|
-
"import { AppRegistry } from 'react-native'\nimport App from './app/App'\n\nAppRegistry.registerComponent('demo', () => App)\n",
|
|
18
|
-
);
|
|
19
|
-
return root;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
test('init writes an entry that requires the app and refuses non-local backends', () => {
|
|
23
|
-
const root = tmpApp();
|
|
24
|
-
const notes = init(root, { entry: 'qa/headless.tsx', apiEnv: 'API_URL', auto: false, force: false });
|
|
25
|
-
const entry = fs.readFileSync(path.join(root, 'qa/headless.tsx'), 'utf8');
|
|
26
|
-
assert.match(entry, /require\('\.\.\/app\/App'\)/);
|
|
27
|
-
assert.match(entry, /process\.env\.API_URL/);
|
|
28
|
-
assert.match(entry, /installDefaultMocks/);
|
|
29
|
-
assert.ok(notes.some((n) => n.includes('wrote qa/headless.tsx')));
|
|
30
|
-
// Second run leaves it alone.
|
|
31
|
-
const again = init(root, { entry: 'qa/headless.tsx', apiEnv: 'API_URL', auto: false, force: false });
|
|
32
|
-
assert.ok(again.some((n) => n.includes('left as is')));
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('init --auto adds the mount after the imports, once', () => {
|
|
36
|
-
const root = tmpApp();
|
|
37
|
-
init(root, { entry: 'qa/headless.tsx', apiEnv: 'API_URL', auto: true, force: false });
|
|
38
|
-
init(root, { entry: 'qa/headless.tsx', apiEnv: 'API_URL', auto: true, force: false });
|
|
39
|
-
const index = fs.readFileSync(path.join(root, 'index.js'), 'utf8');
|
|
40
|
-
assert.equal(index.split('lynkeus-agent/auto').length, 2);
|
|
41
|
-
assert.ok(index.indexOf('auto(') < index.indexOf('registerComponent'));
|
|
42
|
-
assert.ok(index.indexOf("import App from './app/App'") < index.indexOf('lynkeus-agent/auto'));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('the error catalogue names the module behind a TurboModule crash', () => {
|
|
46
|
-
const [e] = explainError(
|
|
47
|
-
"Invariant Violation: TurboModuleRegistry.getEnforcing(...): 'RNCWebView' could not be found. Verify that a module by this name is registered",
|
|
48
|
-
);
|
|
49
|
-
assert.ok(e);
|
|
50
|
-
assert.match(e.cause, /RNCWebView/);
|
|
51
|
-
assert.equal(explainError('all good').length, 0);
|
|
52
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { simulatorArgv } from '../commands/device.js';
|
|
5
|
-
import { draftFlow } from '../commands/record.js';
|
|
6
|
-
import type { TraceEvent } from 'lynkeus-protocol';
|
|
7
|
-
|
|
8
|
-
const ev = (partial: Record<string, unknown>, seq: number): TraceEvent => ({ seq, t: seq * 100, ...partial }) as unknown as TraceEvent;
|
|
9
|
-
|
|
10
|
-
test('a recording becomes presses with what each one caused', () => {
|
|
11
|
-
const flow = draftFlow([
|
|
12
|
-
ev({ kind: 'route', route: 'First.Screen', path: ['First.Screen'] }, 1),
|
|
13
|
-
ev({ kind: 'touch', x: 10, y: 20, testId: 'primary-button', route: 'First.Screen' }, 2),
|
|
14
|
-
ev({ kind: 'request', method: 'GET', url: 'http://x/api/items', status: 200, ms: 40 }, 3),
|
|
15
|
-
ev({ kind: 'route', route: 'Second.Screen', path: ['Second.Screen'] }, 4),
|
|
16
|
-
ev({ kind: 'touch', x: 10, y: 20, text: 'Second option', route: 'Second.Screen' }, 5),
|
|
17
|
-
ev({ kind: 'store', store: 'session', changed: { step: { from: 1, to: 2 } } }, 6),
|
|
18
|
-
ev({ kind: 'touch', x: 5, y: 5, route: 'Second.Screen' }, 7),
|
|
19
|
-
]);
|
|
20
|
-
assert.deepEqual(flow.steps, [
|
|
21
|
-
{ press: { testId: 'primary-button' } },
|
|
22
|
-
{ waitFor: { route: 'Second.Screen', timeoutMs: 10000 } },
|
|
23
|
-
{ note: 'requests: GET /api/items 200' },
|
|
24
|
-
{ press: { text: 'Second option' } },
|
|
25
|
-
{ note: 'stores changed: session.step' },
|
|
26
|
-
{ note: 'touch at 5,5 on Second.Screen hit nothing lynkeus could name' },
|
|
27
|
-
]);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('device subcommands map to simctl and adb', () => {
|
|
31
|
-
assert.deepEqual(simulatorArgv('ios', 'UDID', 'permissions', ['grant', 'camera'], 'com.example.myapp')[0], {
|
|
32
|
-
bin: 'xcrun',
|
|
33
|
-
args: ['simctl', 'privacy', 'UDID', 'grant', 'camera', 'com.example.myapp'],
|
|
34
|
-
});
|
|
35
|
-
assert.deepEqual(simulatorArgv('ios', 'UDID', 'appearance', ['dark'])[0]!.args, ['simctl', 'ui', 'UDID', 'appearance', 'dark']);
|
|
36
|
-
assert.deepEqual(simulatorArgv('ios', 'UDID', 'location', ['-34.6,-58.4'])[0]!.args, ['simctl', 'location', 'UDID', 'set', '-34.6,-58.4']);
|
|
37
|
-
assert.equal(simulatorArgv('ios', 'UDID', 'language', ['de-DE'], 'com.example.myapp').length, 2);
|
|
38
|
-
assert.deepEqual(simulatorArgv('android', 'emu', 'permissions', ['grant', 'camera'], 'com.example.myapp')[0]!.args, [
|
|
39
|
-
'-s',
|
|
40
|
-
'emu',
|
|
41
|
-
'shell',
|
|
42
|
-
'pm',
|
|
43
|
-
'grant',
|
|
44
|
-
'com.example.myapp',
|
|
45
|
-
'android.permission.CAMERA',
|
|
46
|
-
]);
|
|
47
|
-
assert.throws(() => simulatorArgv('ios', 'UDID', 'permissions', ['grant', 'camera']), /--app/);
|
|
48
|
-
assert.throws(() => simulatorArgv('ios', 'UDID', 'appearance', ['blue']), /light\|dark/);
|
|
49
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
import { PNG } from 'pngjs';
|
|
7
|
-
|
|
8
|
-
import { comparePng, snapshot } from '../commands/snapshot.js';
|
|
9
|
-
|
|
10
|
-
const png = (w: number, h: number, paint: (x: number, y: number) => [number, number, number]): Buffer => {
|
|
11
|
-
const p = new PNG({ width: w, height: h });
|
|
12
|
-
for (let y = 0; y < h; y++)
|
|
13
|
-
for (let x = 0; x < w; x++) {
|
|
14
|
-
const i = (y * w + x) * 4;
|
|
15
|
-
const [r, g, b] = paint(x, y);
|
|
16
|
-
p.data[i] = r;
|
|
17
|
-
p.data[i + 1] = g;
|
|
18
|
-
p.data[i + 2] = b;
|
|
19
|
-
p.data[i + 3] = 255;
|
|
20
|
-
}
|
|
21
|
-
return PNG.sync.write(p);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
test('pixels that moved are counted and painted into a diff', () => {
|
|
25
|
-
const a = png(10, 10, () => [255, 255, 255]);
|
|
26
|
-
const b = png(10, 10, (x, y) => (x < 2 && y < 2 ? [0, 0, 0] : [250, 250, 250]));
|
|
27
|
-
const c = comparePng(a, b);
|
|
28
|
-
assert.equal(c.different, 4); // the 250 vs 255 shade stays under the channel threshold
|
|
29
|
-
assert.equal(c.ratio, 0.04);
|
|
30
|
-
assert.ok(c.diff);
|
|
31
|
-
const d = PNG.sync.read(c.diff!);
|
|
32
|
-
assert.deepEqual([d.data[0], d.data[1], d.data[2]], [255, 0, 0]);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('a first capture is the baseline; a small change matches; a big one writes a diff', () => {
|
|
36
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-snap-'));
|
|
37
|
-
const lynkeusDir = path.join(root, '.lynkeus');
|
|
38
|
-
const runsDir = path.join(lynkeusDir, 'runs');
|
|
39
|
-
const white = png(20, 20, () => [255, 255, 255]);
|
|
40
|
-
assert.equal(snapshot(lynkeusDir, 'card', white, { runsDir }).status, 'baseline');
|
|
41
|
-
const nearly = png(20, 20, (x, y) => (x === 0 && y === 0 ? [0, 0, 0] : [255, 255, 255]));
|
|
42
|
-
assert.equal(snapshot(lynkeusDir, 'card', nearly, { runsDir, tolerance: 0.01 }).status, 'match');
|
|
43
|
-
const half = png(20, 20, (x) => (x < 10 ? [0, 0, 0] : [255, 255, 255]));
|
|
44
|
-
const r = snapshot(lynkeusDir, 'card', half, { runsDir });
|
|
45
|
-
assert.equal(r.status, 'mismatch');
|
|
46
|
-
assert.ok(fs.existsSync(r.diffFile!));
|
|
47
|
-
assert.equal(snapshot(lynkeusDir, 'card', half, { runsDir, update: true }).status, 'baseline');
|
|
48
|
-
assert.equal(snapshot(lynkeusDir, 'card', half, { runsDir }).status, 'match');
|
|
49
|
-
const other = png(30, 20, () => [255, 255, 255]);
|
|
50
|
-
assert.ok(snapshot(lynkeusDir, 'card', other, { runsDir }).comparison?.sizeMismatch);
|
|
51
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import type { Screen } from 'lynkeus-protocol';
|
|
5
|
-
|
|
6
|
-
import { describeScreen } from '../device/driver.js';
|
|
7
|
-
import { parseTarget, scopedTarget } from '../targets.js';
|
|
8
|
-
|
|
9
|
-
test('a #-prefixed string is a testId', () => {
|
|
10
|
-
assert.deepEqual(parseTarget('#next-button'), { testId: 'next-button' });
|
|
11
|
-
});
|
|
12
|
-
test('an x,y pair is a point', () => {
|
|
13
|
-
assert.deepEqual(parseTarget('100,200'), { x: 100, y: 200 });
|
|
14
|
-
});
|
|
15
|
-
test('a bare integer is an index', () => {
|
|
16
|
-
assert.deepEqual(parseTarget('12'), { i: 12 });
|
|
17
|
-
});
|
|
18
|
-
test('anything else is text', () => {
|
|
19
|
-
assert.deepEqual(parseTarget('Some label'), { text: 'Some label' });
|
|
20
|
-
});
|
|
21
|
-
test('a testId that contains a comma is still a testId, not a point', () => {
|
|
22
|
-
assert.deepEqual(parseTarget('#a,b'), { testId: 'a,b' });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('describeScreen prints the value a QR image carries', () => {
|
|
26
|
-
const screen = {
|
|
27
|
-
route: 'X',
|
|
28
|
-
path: ['X'],
|
|
29
|
-
commit: 1,
|
|
30
|
-
busy: false,
|
|
31
|
-
window: { w: 390, h: 844 },
|
|
32
|
-
costMs: 1,
|
|
33
|
-
elements: [{ i: 0, kind: 'image', frame: { x: 0, y: 0, w: 168, h: 169 }, enabled: true, depth: 0, accessibilityLabel: 'qr', value: 'QR-TEST-0001' }],
|
|
34
|
-
} as unknown as Screen;
|
|
35
|
-
assert.match(describeScreen(screen), /image .*value="QR-TEST-0001"/);
|
|
36
|
-
});
|
|
37
|
-
test('describeScreen lists a view with a testID and leaves plain layout out', () => {
|
|
38
|
-
const screen = {
|
|
39
|
-
route: 'X',
|
|
40
|
-
path: ['X'],
|
|
41
|
-
commit: 1,
|
|
42
|
-
busy: false,
|
|
43
|
-
window: { w: 390, h: 844 },
|
|
44
|
-
costMs: 1,
|
|
45
|
-
elements: [
|
|
46
|
-
{ i: 0, kind: 'view', frame: { x: 0, y: 0, w: 390, h: 844 }, enabled: true, depth: 0 },
|
|
47
|
-
{ i: 1, kind: 'view', frame: { x: 0, y: 0, w: 390, h: 400 }, enabled: true, depth: 1, testId: 'intro-page-advances' },
|
|
48
|
-
],
|
|
49
|
-
} as unknown as Screen;
|
|
50
|
-
const text = describeScreen(screen);
|
|
51
|
-
assert.match(text, /view +#intro-page-advances/);
|
|
52
|
-
assert.doesNotMatch(text, /^0? ?view$/m);
|
|
53
|
-
});
|
|
54
|
-
test('describeScreen leaves out what is not presented, counts it, and lists it with --all', () => {
|
|
55
|
-
const el = (i: number, over: object) => ({ i, kind: 'button', frame: { x: 0, y: 0, w: 100, h: 40 }, enabled: true, depth: 0, ...over });
|
|
56
|
-
const screen = {
|
|
57
|
-
route: 'X',
|
|
58
|
-
path: ['X'],
|
|
59
|
-
commit: 7,
|
|
60
|
-
busy: false,
|
|
61
|
-
window: { w: 390, h: 844 },
|
|
62
|
-
costMs: 3,
|
|
63
|
-
elements: [
|
|
64
|
-
el(0, { testId: 'tab-a', hidden: 'a11y' }),
|
|
65
|
-
el(1, { testId: 'behind', hidden: 'behind-overlay' }),
|
|
66
|
-
el(2, { testId: 'slide', hidden: 'inert' }),
|
|
67
|
-
el(3, { testId: 'close', enabled: false }),
|
|
68
|
-
],
|
|
69
|
-
} as unknown as Screen;
|
|
70
|
-
const text = describeScreen(screen);
|
|
71
|
-
assert.equal(text.split('\n')[0] ?? '', 'X · 2 hidden: 1 a11y, 1 behind-overlay');
|
|
72
|
-
assert.doesNotMatch(text, /#tab-a|#behind/);
|
|
73
|
-
assert.match(text, /^button #slide ~inert$/m);
|
|
74
|
-
assert.match(text, /^button #close !disabled$/m);
|
|
75
|
-
assert.match(describeScreen(screen, { all: true }), /#behind ~behind-overlay/);
|
|
76
|
-
assert.match(describeScreen(screen, { frames: true }), /#slide ~inert @50,20$/m);
|
|
77
|
-
assert.match(describeScreen(screen, { verbose: true }).split('\n')[0] ?? '', /commit=7 3ms/);
|
|
78
|
-
});
|
|
79
|
-
test("describeScreen leaves a button's icon out", () => {
|
|
80
|
-
const screen = {
|
|
81
|
-
route: 'X',
|
|
82
|
-
path: ['X'],
|
|
83
|
-
commit: 1,
|
|
84
|
-
busy: false,
|
|
85
|
-
window: { w: 390, h: 844 },
|
|
86
|
-
costMs: 1,
|
|
87
|
-
elements: [
|
|
88
|
-
{ i: 0, kind: 'button', frame: { x: 0, y: 0, w: 100, h: 40 }, enabled: true, depth: 0, testId: 'open' },
|
|
89
|
-
{ i: 1, kind: 'view', frame: { x: 0, y: 0, w: 20, h: 20 }, enabled: true, depth: 1, testId: 'SvgArrow', parent: 0 },
|
|
90
|
-
{ i: 2, kind: 'view', frame: { x: 0, y: 50, w: 20, h: 20 }, enabled: true, depth: 0, testId: 'page-marker' },
|
|
91
|
-
],
|
|
92
|
-
} as unknown as Screen;
|
|
93
|
-
assert.doesNotMatch(describeScreen(screen), /SvgArrow/);
|
|
94
|
-
assert.match(describeScreen(screen), /#page-marker/);
|
|
95
|
-
assert.match(describeScreen(screen, { all: true }), /SvgArrow/);
|
|
96
|
-
});
|
|
97
|
-
test('describeScreen gives an index only to what cannot be named apart', () => {
|
|
98
|
-
const el = (i: number, over: object) => ({ i, kind: 'button', frame: { x: 0, y: 0, w: 100, h: 40 }, enabled: true, depth: 0, ...over });
|
|
99
|
-
const screen = {
|
|
100
|
-
route: 'X',
|
|
101
|
-
path: ['X'],
|
|
102
|
-
commit: 1,
|
|
103
|
-
busy: false,
|
|
104
|
-
window: { w: 390, h: 844 },
|
|
105
|
-
costMs: 1,
|
|
106
|
-
elements: [
|
|
107
|
-
el(0, { testId: 'save' }),
|
|
108
|
-
el(1, { testId: 'like' }),
|
|
109
|
-
el(2, { testId: 'like' }),
|
|
110
|
-
el(3, { kind: 'text', text: 'Hello' }),
|
|
111
|
-
el(4, { kind: 'view', testId: 'marker', accessibilityLabel: 'marker' }),
|
|
112
|
-
el(5, {}),
|
|
113
|
-
],
|
|
114
|
-
} as unknown as Screen;
|
|
115
|
-
assert.deepEqual(describeScreen(screen).split('\n').slice(1), [
|
|
116
|
-
'button #save',
|
|
117
|
-
'1 button #like',
|
|
118
|
-
'2 button #like',
|
|
119
|
-
'text "Hello"',
|
|
120
|
-
'view #marker',
|
|
121
|
-
'5 button',
|
|
122
|
-
]);
|
|
123
|
-
});
|
|
124
|
-
test('a [n] suffix picks the nth match of a testId or text', () => {
|
|
125
|
-
assert.deepEqual(parseTarget('#field[2]'), { testId: 'field', nth: 2 });
|
|
126
|
-
assert.deepEqual(parseTarget('Ciudad[0]'), { text: 'Ciudad', nth: 0 });
|
|
127
|
-
});
|
|
128
|
-
test('--in and --nth narrow a target', () => {
|
|
129
|
-
assert.deepEqual(scopedTarget('#like-message-button', { in: 'hi from Ana' }), { testId: 'like-message-button', within: { text: 'hi from Ana' } });
|
|
130
|
-
assert.deepEqual(scopedTarget('#like-message-button', { in: '#feed', nth: 2 }), { testId: 'like-message-button', within: { testId: 'feed' }, nth: 2 });
|
|
131
|
-
assert.deepEqual(scopedTarget('100,200', { in: '#feed' }), { x: 100, y: 200 });
|
|
132
|
-
});
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
|
|
8
|
-
import { dig, interpolate, matcherOf, matches, parseCase, readCases } from '../format.js';
|
|
9
|
-
|
|
10
|
-
const CASE = `---
|
|
11
|
-
id: gift
|
|
12
|
-
title: "A gift shows up"
|
|
13
|
-
---
|
|
14
|
-
setup:
|
|
15
|
-
- create_user: { as: u, name: "Ana" }
|
|
16
|
-
steps:
|
|
17
|
-
- app.press: "#open"
|
|
18
|
-
- app.back
|
|
19
|
-
- assert: { user: u, path: score, equals: 25 }
|
|
20
|
-
expected: |
|
|
21
|
-
The score is 25.
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
test('a case is its frontmatter, its setup, its steps and what it expects', () => {
|
|
25
|
-
const c = parseCase(CASE, 'cases/gift.md');
|
|
26
|
-
assert.equal(c.id, 'gift');
|
|
27
|
-
assert.deepEqual(c.setup, [{ verb: 'create_user', value: { as: 'u', name: 'Ana' } }]);
|
|
28
|
-
assert.deepEqual(
|
|
29
|
-
c.steps.map((s) => s.verb),
|
|
30
|
-
['app.press', 'app.back', 'assert'],
|
|
31
|
-
);
|
|
32
|
-
assert.equal(c.expected, 'The score is 25.');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('a file without frontmatter, or a step with two verbs, is refused with its place', () => {
|
|
36
|
-
assert.throws(() => parseCase('steps: []', 'x.md'), /x\.md: no frontmatter/);
|
|
37
|
-
assert.throws(() => parseCase('---\nid: x\n---\nsteps:\n - { a: 1, b: 2 }\n', 'x.md'), /steps\[0\] must be one step/);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('a path reads into lists, and a star collects from every element', () => {
|
|
41
|
-
const json = { accounts: [{ code: 'usd' }, { code: 'eur' }], user: { id: 7 } };
|
|
42
|
-
assert.equal(dig(json, 'accounts.1.code'), 'eur');
|
|
43
|
-
assert.deepEqual(dig(json, 'accounts.*.code'), ['usd', 'eur']);
|
|
44
|
-
assert.equal(dig(json, 'user.name.first'), undefined);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('a reference alone keeps its type; inside text it is text; a missing one says which', () => {
|
|
48
|
-
const scope = { u: { user_id: 7, name: 'Ana' } };
|
|
49
|
-
assert.deepEqual(interpolate({ id: '{{u.user_id}}', hi: 'hi {{u.name}}', n: 3 }, scope), { id: 7, hi: 'hi Ana', n: 3 });
|
|
50
|
-
assert.match(interpolate('x{{random.letters}}', scope), /^x[a-z]{6}$/);
|
|
51
|
-
assert.throws(() => interpolate('{{v.user_id}}', scope), /no alias 'v'/);
|
|
52
|
-
assert.throws(() => interpolate('{{u.phone}}', scope), /\{\{u\.phone\}\} is empty/);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('matchers compare numbers as numbers and lists by any element', () => {
|
|
56
|
-
assert.equal(matches('25.0', { equals: 25 }), true);
|
|
57
|
-
assert.equal(matches('abc', { equals: 'abc' }), true);
|
|
58
|
-
assert.equal(matches(['usd', 'bridge_usd'], { includes: 'bridge' }), true);
|
|
59
|
-
assert.equal(matches(9, { gte: 10 }), false);
|
|
60
|
-
assert.equal(matches(null, { absent: true }), true);
|
|
61
|
-
assert.equal(matches('AUTORIZADO', { matches: '^AUT' }), true);
|
|
62
|
-
assert.deepEqual(matcherOf({ path: 'a', equals: 1, user: 'u' }), { equals: 1 });
|
|
63
|
-
assert.equal(matcherOf({ path: 'a' }), undefined);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('a case with a matrix runs once per combination, named after it', () => {
|
|
67
|
-
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-matrix-')), 'm.md');
|
|
68
|
-
fs.writeFileSync(file, '---\nid: m\nmatrix: { locale: [en, fr], login: [phone] }\n---\nsteps:\n - create_user: { locale: "{{matrix.locale}}" }\n');
|
|
69
|
-
const cases = readCases(file);
|
|
70
|
-
assert.deepEqual(
|
|
71
|
-
cases.map((c) => c.id),
|
|
72
|
-
['m[locale=en,login=phone]', 'm[locale=fr,login=phone]'],
|
|
73
|
-
);
|
|
74
|
-
assert.deepEqual(cases[1]!.setup, []);
|
|
75
|
-
assert.deepEqual(cases[1]!.steps[0]!.value, { locale: 'fr' });
|
|
76
|
-
});
|