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,230 +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 { after, test } from 'node:test';
|
|
6
|
-
|
|
7
|
-
import { driveCommands } from '../../commands/drive.js';
|
|
8
|
-
import { osCommands } from '../../commands/os.js';
|
|
9
|
-
import type { Device } from '../../device/driver.js';
|
|
10
|
-
import { type Base, find, register } from '../../registry.js';
|
|
11
|
-
import { Fixtures } from '../fixtures.js';
|
|
12
|
-
import { parseCase } from '../format.js';
|
|
13
|
-
import { argvFor, newSince, runCase } from '../runner.js';
|
|
14
|
-
import { FIXTURES } from '../templates.js';
|
|
15
|
-
|
|
16
|
-
register([...driveCommands, ...osCommands]);
|
|
17
|
-
|
|
18
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-cases-'));
|
|
19
|
-
fs.writeFileSync(path.join(root, 'fixtures.mjs'), FIXTURES);
|
|
20
|
-
const fixtures = new Fixtures({ command: 'node fixtures.mjs' }, root);
|
|
21
|
-
after(() => fixtures.close());
|
|
22
|
-
|
|
23
|
-
type Call = { method: string; params: unknown };
|
|
24
|
-
|
|
25
|
-
// An app with one button that, pressed, shows the points the fixtures hold.
|
|
26
|
-
const fakeApp = () => {
|
|
27
|
-
const calls: Call[] = [];
|
|
28
|
-
let shown = 'Hello';
|
|
29
|
-
const screen = () => ({
|
|
30
|
-
route: 'Profile.Main',
|
|
31
|
-
path: ['Profile.Main'],
|
|
32
|
-
commit: 1,
|
|
33
|
-
busy: false,
|
|
34
|
-
window: { w: 390, h: 844 },
|
|
35
|
-
costMs: 1,
|
|
36
|
-
elements: [
|
|
37
|
-
{ i: 0, kind: 'button', testId: 'refresh', enabled: true, depth: 0, frame: { x: 0, y: 0, w: 100, h: 40 } },
|
|
38
|
-
{ i: 1, kind: 'text', text: shown, enabled: true, depth: 0, frame: { x: 0, y: 50, w: 100, h: 20 } },
|
|
39
|
-
],
|
|
40
|
-
});
|
|
41
|
-
const server = {
|
|
42
|
-
connected: true,
|
|
43
|
-
call: async (method: string, params: { testId?: string }) => {
|
|
44
|
-
calls.push({ method, params });
|
|
45
|
-
if (method === 'find') return screen().elements.find((e) => e.testId === params.testId) ?? null;
|
|
46
|
-
return null;
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
const device = {
|
|
50
|
-
server,
|
|
51
|
-
screen: async () => screen(),
|
|
52
|
-
trace: async () => ({ last: 0, events: [{ seq: 1, t: 0, kind: 'request', method: 'GET', url: 'http://x/api/points', status: 200 }] }),
|
|
53
|
-
press: async (params: unknown) => {
|
|
54
|
-
calls.push({ method: 'press', params });
|
|
55
|
-
shown = '25 points';
|
|
56
|
-
return { x: 0, y: 0, mode: 'native' };
|
|
57
|
-
},
|
|
58
|
-
waitFor: async () => ({ waitedMs: 0 }),
|
|
59
|
-
idle: async () => ({ settled: true }),
|
|
60
|
-
command: async (name: string) => (name === 'events' ? [{ name: 'points_viewed', properties: { points: 25 } }] : {}),
|
|
61
|
-
} as unknown as Device;
|
|
62
|
-
const base: Base = {
|
|
63
|
-
root,
|
|
64
|
-
config: {},
|
|
65
|
-
attached: true,
|
|
66
|
-
err: () => undefined,
|
|
67
|
-
graph: () => {
|
|
68
|
-
throw new Error('no graph');
|
|
69
|
-
},
|
|
70
|
-
device: async () => device,
|
|
71
|
-
};
|
|
72
|
-
return { base, calls };
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const config = { refs: { user: 'user_id' }, inspect: 'inspect' };
|
|
76
|
-
|
|
77
|
-
test('a case prepares the backend, drives the app and reads both', async () => {
|
|
78
|
-
const c = parseCase(
|
|
79
|
-
`---
|
|
80
|
-
id: gift
|
|
81
|
-
---
|
|
82
|
-
setup:
|
|
83
|
-
- create_user: { as: u, name: "Ana" }
|
|
84
|
-
steps:
|
|
85
|
-
- app.see: { text: "Hello" }
|
|
86
|
-
- give_points: { user: u, points: 25 }
|
|
87
|
-
- app.press: "#refresh"
|
|
88
|
-
- app.see: { text: "25 points", request: "GET /api/points", event: points_viewed, props: { points: 25 } }
|
|
89
|
-
- app.see: { text: "Hello", absent: true }
|
|
90
|
-
- assert: { user: u, path: points, equals: 25 }
|
|
91
|
-
- assert: { path: name, equals: "Ana" }
|
|
92
|
-
`,
|
|
93
|
-
'gift.md',
|
|
94
|
-
);
|
|
95
|
-
const { base, calls } = fakeApp();
|
|
96
|
-
const report = await runCase(c, { base, config, fixtures });
|
|
97
|
-
assert.deepEqual(
|
|
98
|
-
report.steps.map((s) => [s.status, s.error]),
|
|
99
|
-
Array(8).fill(['passed', undefined]),
|
|
100
|
-
);
|
|
101
|
-
assert.equal(calls.filter((x) => x.method === 'press').length, 1);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('the first step that fails stops the case and leaves the evidence', async () => {
|
|
105
|
-
const c = parseCase('---\nid: red\n---\nsteps:\n - app.see: { text: "Goodbye", timeoutMs: 200 }\n - app.press: "#refresh"\n', 'red.md');
|
|
106
|
-
const { base, calls } = fakeApp();
|
|
107
|
-
const report = await runCase(c, { base, config, fixtures });
|
|
108
|
-
assert.equal(report.result, 'failed');
|
|
109
|
-
assert.deepEqual(
|
|
110
|
-
report.steps.map((s) => s.status),
|
|
111
|
-
['failed', 'skipped'],
|
|
112
|
-
);
|
|
113
|
-
assert.match(report.steps[0]!.error!, /not on screen: "Goodbye"/);
|
|
114
|
-
assert.match(report.evidence!.screen!, /button #refresh/);
|
|
115
|
-
assert.equal(calls.filter((x) => x.method === 'press').length, 0);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('an optional press of something that is not there is skipped, not failed', async () => {
|
|
119
|
-
const c = parseCase('---\nid: opt\n---\nsteps:\n - app.press: { target: "#dismiss", optional: true }\n', 'opt.md');
|
|
120
|
-
const { base } = fakeApp();
|
|
121
|
-
const device = await base.device();
|
|
122
|
-
(device as unknown as { waitFor: () => Promise<never> }).waitFor = async () => {
|
|
123
|
-
throw new Error('Timed out');
|
|
124
|
-
};
|
|
125
|
-
const report = await runCase(c, { base, config, fixtures });
|
|
126
|
-
assert.deepEqual(
|
|
127
|
-
report.steps.map((s) => s.status),
|
|
128
|
-
['skipped'],
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('a macro is the steps a project gave a name to, with what the case passed', async () => {
|
|
133
|
-
const c = parseCase(
|
|
134
|
-
'---\nid: m\n---\nsetup:\n - create_user: { as: u }\nsteps:\n - app.enter: { as: u, then: "#refresh" }\n - app.enter: { as: u }\n',
|
|
135
|
-
'm.md',
|
|
136
|
-
);
|
|
137
|
-
const { base, calls } = fakeApp();
|
|
138
|
-
const macros = {
|
|
139
|
-
'app.enter': [{ assert: { user_id: '{{param.as.user_id}}', path: 'user_id', matches: '^u_' } }, { when: 'then', do: { 'app.press': '{{param.then}}' } }],
|
|
140
|
-
};
|
|
141
|
-
const report = await runCase(c, { base, config: { ...config, macros }, fixtures });
|
|
142
|
-
assert.equal(report.result, 'passed', JSON.stringify(report.steps));
|
|
143
|
-
assert.equal(calls.filter((x) => x.method === 'press').length, 1);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("a step that is neither the app's nor a declared fixture says so", async () => {
|
|
147
|
-
const c = parseCase('---\nid: f\n---\nsteps:\n - teleport: { to: "moon" }\n', 'f.md');
|
|
148
|
-
const { base } = fakeApp();
|
|
149
|
-
const report = await runCase(c, { base, config, fixtures });
|
|
150
|
-
assert.match(report.steps[0]!.error!, /no fixture teleport/);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test('a step becomes the argv of the command it names', () => {
|
|
154
|
-
assert.deepEqual(argvFor(find('type')!, { text: 'ana', target: '#search', submit: true }), ['type', 'ana', '#search', '--submit']);
|
|
155
|
-
assert.deepEqual(argvFor(find('wait')!, { target: '#row-25', scroll: true, timeoutMs: 9000 }), ['wait', '#row-25', '--scroll', '--timeout', '9000']);
|
|
156
|
-
assert.deepEqual(argvFor(find('press')!, { target: '#like', within: 'hi from Ana' }), ['press', '#like', '--in', 'hi from Ana']);
|
|
157
|
-
assert.deepEqual(argvFor(find('mock')!, { request: 'POST /pay', lost: true, times: 1 }), ['mock', 'POST /pay', '--lost', '--times', '1']);
|
|
158
|
-
assert.deepEqual(argvFor(find('swipe')!, { from: '10,20', to: '10,300', durationMs: 300 }), [
|
|
159
|
-
'swipe',
|
|
160
|
-
'--from',
|
|
161
|
-
'10,20',
|
|
162
|
-
'--to',
|
|
163
|
-
'10,300',
|
|
164
|
-
'--duration',
|
|
165
|
-
'300',
|
|
166
|
-
]);
|
|
167
|
-
assert.deepEqual(argvFor(find('mock')!, 'clear'), ['mock', '--clear']);
|
|
168
|
-
assert.throws(() => argvFor(find('press')!, { target: '#x', sideways: true }), /press has no sideways/);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('what a buffer gained is found whichever end it grows from', () => {
|
|
172
|
-
const a = { name: 'a' };
|
|
173
|
-
const b = { name: 'b' };
|
|
174
|
-
const c = { name: 'c' };
|
|
175
|
-
assert.deepEqual(newSince([a], [a, b, c]), [b, c]);
|
|
176
|
-
assert.deepEqual(newSince([a], [c, b, a]), [b, c]);
|
|
177
|
-
assert.deepEqual(newSince([a, b], [a, b]), []);
|
|
178
|
-
assert.deepEqual(newSince([a, b], [c]), [c]);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test('a run that asks for events keeps each with the step that caused it', async () => {
|
|
182
|
-
const c = parseCase('---\nid: ev\n---\nsteps:\n - app.see: { text: "Hello" }\n - app.press: "#refresh"\n', 'ev.md');
|
|
183
|
-
const { base } = fakeApp();
|
|
184
|
-
const device = await base.device();
|
|
185
|
-
let buffer: object[] = [];
|
|
186
|
-
(device as unknown as { command: (n: string) => Promise<unknown> }).command = async () => buffer;
|
|
187
|
-
const press = device.press.bind(device);
|
|
188
|
-
(device as unknown as { press: typeof device.press }).press = async (p) => {
|
|
189
|
-
buffer = [{ name: 'refreshed', properties: { by: 'button' } }, ...buffer];
|
|
190
|
-
return press(p);
|
|
191
|
-
};
|
|
192
|
-
const report = await runCase(c, { base, config, fixtures, events: true });
|
|
193
|
-
assert.deepEqual(report.events, [{ name: 'refreshed', props: { by: 'button' }, step: 2, route: 'Profile.Main' }]);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test('what the app puts in the way of a step is dealt with before it, unless the step is about it', async () => {
|
|
197
|
-
const { base, calls } = fakeApp();
|
|
198
|
-
const interruptions = [{ see: '#refresh', unless: 'refresh', do: [{ 'app.press': '#refresh' }] }];
|
|
199
|
-
const c = parseCase('---\nid: t\n---\nsteps:\n - app.see: { testId: refresh }\n - app.see: { text: "25 points" }\n', 't.md');
|
|
200
|
-
const report = await runCase(c, { base, config: { ...config, interruptions }, fixtures });
|
|
201
|
-
assert.equal(report.result, 'passed');
|
|
202
|
-
assert.equal(calls.filter((call) => call.method === 'press').length > 0, true);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('a dry run names the step nobody answers and the argument a command does not take', async () => {
|
|
206
|
-
const { base } = fakeApp();
|
|
207
|
-
const c = parseCase('---\nid: t\n---\nsteps:\n - app.teleport: somewhere\n - app.clock: { sideways: 1 }\n - app.clock: { advanceMs: 1000 }\n', 't.md');
|
|
208
|
-
const report = await runCase(c, { base, config, fixtures, dry: true });
|
|
209
|
-
assert.equal(report.result, 'failed');
|
|
210
|
-
assert.deepEqual(
|
|
211
|
-
report.steps.map((s) => s.status),
|
|
212
|
-
['failed', 'failed', 'passed'],
|
|
213
|
-
);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test('a step kept to a device is skipped on a host, and one kept to the host runs there', async () => {
|
|
217
|
-
const { base, calls } = fakeApp();
|
|
218
|
-
const c = parseCase(
|
|
219
|
-
'---\nid: t\n---\nsteps:\n - on: device\n do:\n - app.press: "#nowhere"\n - on: headless\n do: { app.press: "#refresh" }\n - app.see: { text: "25 points" }\n',
|
|
220
|
-
't.md',
|
|
221
|
-
);
|
|
222
|
-
assert.equal(c.steps[0]!.on, 'device');
|
|
223
|
-
const report = await runCase(c, { base, config, fixtures });
|
|
224
|
-
assert.equal(report.result, 'passed', JSON.stringify(report.steps));
|
|
225
|
-
assert.deepEqual(
|
|
226
|
-
report.steps.map((s) => s.status),
|
|
227
|
-
['skipped', 'passed', 'passed'],
|
|
228
|
-
);
|
|
229
|
-
assert.equal(calls.filter((call) => call.method === 'press').length, 1);
|
|
230
|
-
});
|
|
@@ -1,34 +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 * as edges from '../edges.js';
|
|
8
|
-
|
|
9
|
-
test('a sighting is one appended line; reading merges them and compaction keeps one per edge', () => {
|
|
10
|
-
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-edges-')), 'edges.jsonl');
|
|
11
|
-
const store = edges.open(file);
|
|
12
|
-
assert.equal(store.record({ from: 'B.Main', to: 'C.Main', via: 'next' }), true);
|
|
13
|
-
assert.equal(store.record({ from: 'A.Main', to: 'B.Main', via: 'open' }), true);
|
|
14
|
-
assert.equal(store.record({ from: 'B.Main', to: 'C.Main', via: 'next' }), false);
|
|
15
|
-
assert.equal(fs.readFileSync(file, 'utf8').trim().split('\n').length, 3);
|
|
16
|
-
assert.deepEqual(
|
|
17
|
-
edges.load(file).map((e) => [e.from, e.seen]),
|
|
18
|
-
[
|
|
19
|
-
['B.Main', 2],
|
|
20
|
-
['A.Main', 1],
|
|
21
|
-
],
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
store.compact();
|
|
25
|
-
assert.deepEqual(
|
|
26
|
-
fs
|
|
27
|
-
.readFileSync(file, 'utf8')
|
|
28
|
-
.trim()
|
|
29
|
-
.split('\n')
|
|
30
|
-
.map((l) => JSON.parse(l).from),
|
|
31
|
-
['A.Main', 'B.Main'],
|
|
32
|
-
);
|
|
33
|
-
assert.equal(edges.prune(file, new Set(['A.Main', 'B.Main'])), 1);
|
|
34
|
-
});
|
|
@@ -1,45 +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 type { Device } from '../../device/driver.js';
|
|
8
|
-
import { runFlow } from '../../flow/runner.js';
|
|
9
|
-
import { eventsFile, normalizeEvents, readEvents } from '../events.js';
|
|
10
|
-
|
|
11
|
-
test('an app buffer that says properties reads as props', () => {
|
|
12
|
-
assert.deepEqual(normalizeEvents([{ name: 'a', properties: { x: 1 } }, { name: 'b', props: { y: 2 } }, { nope: true }]), [
|
|
13
|
-
{ name: 'a', props: { x: 1 } },
|
|
14
|
-
{ name: 'b', props: { y: 2 } },
|
|
15
|
-
]);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('a run with events keeps each one with the step and route that caused it', async () => {
|
|
19
|
-
let buffer: unknown[] = [{ name: 'app_opened' }];
|
|
20
|
-
const routes = ['Checkout.Main', 'Checkout.Confirm'];
|
|
21
|
-
let step = 0;
|
|
22
|
-
const device = {
|
|
23
|
-
server: { connected: true },
|
|
24
|
-
press: async () => {
|
|
25
|
-
buffer.push({ name: step === 0 ? 'checkout_started' : 'checkout_confirmed', properties: { step } });
|
|
26
|
-
step += 1;
|
|
27
|
-
},
|
|
28
|
-
screen: async () => ({ route: routes[step - 1], path: [], elements: [], commit: 0, busy: false, window: { w: 1, h: 1 }, costMs: 0 }),
|
|
29
|
-
command: async (name: string, params?: { clear?: boolean }) => {
|
|
30
|
-
assert.equal(name, 'events');
|
|
31
|
-
const out = buffer;
|
|
32
|
-
if (params?.clear) buffer = [];
|
|
33
|
-
return out;
|
|
34
|
-
},
|
|
35
|
-
} as unknown as Device;
|
|
36
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-events-'));
|
|
37
|
-
const report = await runFlow(
|
|
38
|
-
{ name: 'checkout', steps: [{ press: { testId: 'start' } }, { press: { testId: 'confirm' } }] },
|
|
39
|
-
{ device, artifactsRoot: root, events: true },
|
|
40
|
-
);
|
|
41
|
-
assert.deepEqual(readEvents(eventsFile(report.dir!)), [
|
|
42
|
-
{ name: 'checkout_started', props: { step: 0 }, step: 1, route: 'Checkout.Main' },
|
|
43
|
-
{ name: 'checkout_confirmed', props: { step: 1 }, step: 2, route: 'Checkout.Confirm' },
|
|
44
|
-
]);
|
|
45
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
import { Project } from 'ts-morph';
|
|
4
|
-
|
|
5
|
-
import { collectImports, screenDeclarations } from '../extract.js';
|
|
6
|
-
|
|
7
|
-
const file = (code: string) => new Project({ useInMemoryFileSystem: true }).createSourceFile('Nav.tsx', code);
|
|
8
|
-
|
|
9
|
-
test('JSX screens are found', () => {
|
|
10
|
-
const decls = screenDeclarations(
|
|
11
|
-
file(`
|
|
12
|
-
const Nav = () => <Stack.Navigator><Stack.Screen name="A.Main" component={AMain} /></Stack.Navigator>
|
|
13
|
-
`),
|
|
14
|
-
);
|
|
15
|
-
assert.deepEqual(decls, [{ route: 'A.Main', component: 'AMain' }]);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('static-config screens are found, with quoted and bare keys', () => {
|
|
19
|
-
const decls = screenDeclarations(
|
|
20
|
-
file(`
|
|
21
|
-
const Tabs = createTabs({ screens: {
|
|
22
|
-
'Settings.Notifications': { screen: SettingsNotifications, options: { lazy: false } },
|
|
23
|
-
Settings: { screen: SettingsMain },
|
|
24
|
-
} })
|
|
25
|
-
`),
|
|
26
|
-
);
|
|
27
|
-
assert.deepEqual(decls, [
|
|
28
|
-
{ route: 'Settings.Notifications', component: 'SettingsNotifications' },
|
|
29
|
-
{ route: 'Settings', component: 'SettingsMain' },
|
|
30
|
-
]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('a static entry whose screen is a nested navigator call is not a screen', () => {
|
|
34
|
-
const decls = screenDeclarations(
|
|
35
|
-
file(`
|
|
36
|
-
const Root = createStack({ screens: { 'Root.Tabs': { screen: buildRootTabs({ hideFirst: false }) } } })
|
|
37
|
-
`),
|
|
38
|
-
);
|
|
39
|
-
assert.deepEqual(decls, []);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('an object with a screen key inside a spread still counts', () => {
|
|
43
|
-
const decls = screenDeclarations(
|
|
44
|
-
file(`
|
|
45
|
-
const tabs = { ...(hide ? {} : { 'Settings.Main': { screen: SettingsMain } }) }
|
|
46
|
-
`),
|
|
47
|
-
);
|
|
48
|
-
assert.deepEqual(decls, [{ route: 'Settings.Main', component: 'SettingsMain' }]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('navigate params that name a screen are not declarations', () => {
|
|
52
|
-
const decls = screenDeclarations(
|
|
53
|
-
file(`
|
|
54
|
-
navigation.navigate('Root.Tabs', { screen: 'Settings.Main', params: { screen: 'Settings.Main' } })
|
|
55
|
-
const target = { name: 'Root.Tabs', params: { screen: 'Settings.Main' } }
|
|
56
|
-
`),
|
|
57
|
-
);
|
|
58
|
-
assert.deepEqual(decls, []);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const app = (files: Record<string, string>) => {
|
|
62
|
-
const project = new Project({ useInMemoryFileSystem: true });
|
|
63
|
-
for (const [name, code] of Object.entries(files)) project.createSourceFile(`/app/${name}`, code);
|
|
64
|
-
return (name: string) => collectImports(project.getSourceFileOrThrow(`/app/${name}`), '/app');
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
test('a name imported through a barrel points at the file that defines it', () => {
|
|
68
|
-
const imports = app({
|
|
69
|
-
'hooks/useThing.ts': 'const useThing = () => 1\nexport { useThing }',
|
|
70
|
-
'hooks/useOther.ts': 'export const useOther = () => 2',
|
|
71
|
-
'hooks/index.ts': "export * from './useThing'\nexport { useOther as useRenamed } from './useOther'",
|
|
72
|
-
'Screen.tsx': "import { useThing } from './hooks'",
|
|
73
|
-
'Other.tsx': "import { useRenamed } from './hooks'",
|
|
74
|
-
});
|
|
75
|
-
assert.deepEqual(imports('Screen.tsx'), { imports: ['hooks/useThing.ts'], barrels: ['hooks/index.ts'] });
|
|
76
|
-
assert.deepEqual(imports('Other.tsx'), { imports: ['hooks/useOther.ts'], barrels: ['hooks/index.ts'] });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('what cannot be traced to one file stays on the module it was imported from', () => {
|
|
80
|
-
const imports = app({
|
|
81
|
-
'hooks/useThing.ts': 'export const useThing = () => 1',
|
|
82
|
-
'hooks/index.ts': "export * from './useThing'\nexport const here = 1",
|
|
83
|
-
'Whole.tsx': "import * as hooks from './hooks'",
|
|
84
|
-
'Local.tsx': "import { here, missing } from './hooks'",
|
|
85
|
-
});
|
|
86
|
-
assert.deepEqual(imports('Whole.tsx'), { imports: ['hooks/index.ts'], barrels: [] });
|
|
87
|
-
assert.deepEqual(imports('Local.tsx'), { imports: ['hooks/index.ts'], barrels: [] });
|
|
88
|
-
});
|
|
@@ -1,77 +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 * as frontier from '../frontier.js';
|
|
8
|
-
|
|
9
|
-
const tmpFile = () => path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-frontier-')), 'frontier.jsonl');
|
|
10
|
-
|
|
11
|
-
test('a place owes what it has not tried', () => {
|
|
12
|
-
const file = tmpFile();
|
|
13
|
-
const memory = frontier.open(file);
|
|
14
|
-
memory.record('Settings.Main', { seen: ['share-button', 'account-button', 'profile-button'], visited: true });
|
|
15
|
-
memory.record('Settings.Main', { tried: ['share-button'] });
|
|
16
|
-
|
|
17
|
-
const [row] = frontier.load(file);
|
|
18
|
-
assert.deepEqual(row?.tried, ['share-button']);
|
|
19
|
-
assert.deepEqual(row?.left, ['account-button', 'profile-button']);
|
|
20
|
-
assert.deepEqual(memory.tried('Settings.Main'), ['share-button']);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('a place with nothing left is not offered again', () => {
|
|
24
|
-
const memory = frontier.open(tmpFile());
|
|
25
|
-
memory.record('Settings.Main', { seen: ['a'], visited: true });
|
|
26
|
-
memory.record('Profile.Main', { seen: ['b', 'c'], visited: true });
|
|
27
|
-
memory.record('Settings.Main', { tried: ['a'] });
|
|
28
|
-
|
|
29
|
-
assert.deepEqual(memory.unfinished(), ['Profile.Main']);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('a place only ever seen from outside owes a first visit, before any remainder', () => {
|
|
33
|
-
const memory = frontier.open(tmpFile());
|
|
34
|
-
memory.record('Settings.Main', { seen: ['a', 'b', 'c'], visited: true });
|
|
35
|
-
memory.record('Settings.Main', { tried: ['a'] });
|
|
36
|
-
memory.record('Profile.Edit', {});
|
|
37
|
-
|
|
38
|
-
assert.deepEqual(memory.unfinished(), ['Profile.Edit', 'Settings.Main']);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('a layer is a place of its own', () => {
|
|
42
|
-
const memory = frontier.open(tmpFile());
|
|
43
|
-
memory.record('Settings.Main#account-button', { seen: ['nested-button'], visited: true });
|
|
44
|
-
memory.record('Settings.Main', { seen: ['account-button'], tried: ['account-button'], visited: true });
|
|
45
|
-
|
|
46
|
-
assert.deepEqual(memory.unfinished(), ['Settings.Main#account-button']);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('a run appends as it goes and compacts to one sorted line per place', () => {
|
|
50
|
-
const file = tmpFile();
|
|
51
|
-
const memory = frontier.open(file);
|
|
52
|
-
memory.record('Settings.Main', { seen: ['a', 'b'], visited: true });
|
|
53
|
-
memory.record('Settings.Main', { tried: ['a'] });
|
|
54
|
-
memory.record('Account.Main', { seen: ['c'] });
|
|
55
|
-
assert.equal(fs.readFileSync(file, 'utf8').trim().split('\n').length, 3);
|
|
56
|
-
assert.deepEqual(frontier.open(file).tried('Settings.Main'), ['a']);
|
|
57
|
-
|
|
58
|
-
memory.compact();
|
|
59
|
-
const lines = fs.readFileSync(file, 'utf8').trim().split('\n');
|
|
60
|
-
assert.deepEqual(
|
|
61
|
-
lines.map((l) => JSON.parse(l).node),
|
|
62
|
-
['Account.Main', 'Settings.Main'],
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('a deleted screen takes its unfinished controls with it', () => {
|
|
67
|
-
const file = tmpFile();
|
|
68
|
-
const memory = frontier.open(file);
|
|
69
|
-
memory.record('Settings.Main', { seen: ['a'], visited: true });
|
|
70
|
-
memory.record('Gone.Screen#sheet', { seen: ['b'], visited: true });
|
|
71
|
-
|
|
72
|
-
assert.equal(frontier.prune(file, new Set(['Settings.Main'])), 1);
|
|
73
|
-
assert.deepEqual(
|
|
74
|
-
frontier.load(file).map((r) => r.node),
|
|
75
|
-
['Settings.Main'],
|
|
76
|
-
);
|
|
77
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { checkFunnel, diffEvents, funnelCoverage, type Funnel } from '../funnels.js';
|
|
5
|
-
|
|
6
|
-
const signup: Funnel = {
|
|
7
|
-
name: 'signup',
|
|
8
|
-
steps: [
|
|
9
|
-
{ event: 'signup_started', props: { source: 'string', step: 'number' } },
|
|
10
|
-
{ event: 'signup_completed', props: { method: 'email' } },
|
|
11
|
-
],
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
test('a run that produced every step in order, with the right shapes, passes', () => {
|
|
15
|
-
const result = checkFunnel(signup, [
|
|
16
|
-
{ name: 'app_opened', step: 1 },
|
|
17
|
-
{ name: 'signup_started', props: { source: 'ad', step: 2 }, step: 2 },
|
|
18
|
-
{ name: 'signup_completed', props: { method: 'email' }, step: 4 },
|
|
19
|
-
]);
|
|
20
|
-
assert.equal(result.ok, true);
|
|
21
|
-
assert.deepEqual(
|
|
22
|
-
result.steps.map((s) => s.at),
|
|
23
|
-
[2, 4],
|
|
24
|
-
);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('a missing step, a step out of order and a property of the wrong type each say so', () => {
|
|
28
|
-
assert.equal(checkFunnel(signup, [{ name: 'signup_started', props: { source: 'ad', step: 2 } }]).steps[1]?.problem, 'never happened');
|
|
29
|
-
assert.equal(
|
|
30
|
-
checkFunnel(signup, [
|
|
31
|
-
{ name: 'signup_completed', props: { method: 'email' } },
|
|
32
|
-
{ name: 'signup_started', props: { source: 'ad', step: 2 } },
|
|
33
|
-
]).steps[1]?.problem,
|
|
34
|
-
'happened, but before the step it should follow',
|
|
35
|
-
);
|
|
36
|
-
assert.equal(
|
|
37
|
-
checkFunnel(signup, [
|
|
38
|
-
{ name: 'signup_started', props: { source: 'ad', step: '2' } },
|
|
39
|
-
{ name: 'signup_completed', props: { method: 'phone' } },
|
|
40
|
-
])
|
|
41
|
-
.steps.map((s) => s.problem)
|
|
42
|
-
.join(' | '),
|
|
43
|
-
'step is string, the funnel needs number | method is "phone", the funnel filters on "email"',
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('between two builds: an event that appeared, one that was lost, one whose properties changed', () => {
|
|
48
|
-
const diff = diffEvents([{ name: 'a', props: { step: 1 } }, { name: 'gone' }], [{ name: 'a', props: { step: '1', ref: 'x' } }, { name: 'new' }]);
|
|
49
|
-
assert.deepEqual(diff, { added: ['new'], lost: ['gone'], changed: [{ event: 'a', changes: ['+ref', 'step number→string'] }] });
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('coverage names the funnel steps no run produces', () => {
|
|
53
|
-
const coverage = funnelCoverage([signup], [{ run: 'signup/1', events: [{ name: 'signup_started' }] }]);
|
|
54
|
-
assert.deepEqual(
|
|
55
|
-
coverage[0]?.steps.map((s) => s.runs.length),
|
|
56
|
-
[1, 0],
|
|
57
|
-
);
|
|
58
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { lintScreen } from '../lint.js';
|
|
5
|
-
import type { Screen } from 'lynkeus-protocol';
|
|
6
|
-
|
|
7
|
-
const screen = (elements: Partial<Screen['elements'][number]>[]): Screen =>
|
|
8
|
-
({
|
|
9
|
-
path: ['Root'],
|
|
10
|
-
commit: 1,
|
|
11
|
-
busy: false,
|
|
12
|
-
window: { w: 390, h: 844 },
|
|
13
|
-
costMs: 1,
|
|
14
|
-
elements: elements.map((e, i) => ({ i, kind: 'view', frame: { x: 0, y: 0, w: 100, h: 50 }, enabled: true, depth: 0, ...e })) as Screen['elements'],
|
|
15
|
-
}) as Screen;
|
|
16
|
-
|
|
17
|
-
test('an unlabeled button is flagged', () => {
|
|
18
|
-
const f = lintScreen(screen([{ kind: 'button' }]));
|
|
19
|
-
assert.equal(f.filter((x) => x.rule === 'unlabeled-control').length, 1);
|
|
20
|
-
});
|
|
21
|
-
test('a labeled button is not', () => {
|
|
22
|
-
const f = lintScreen(screen([{ kind: 'button', testId: 'ok' }]));
|
|
23
|
-
assert.equal(f.filter((x) => x.rule === 'unlabeled-control').length, 0);
|
|
24
|
-
});
|
|
25
|
-
test('an icon-only small target warns, a labeled one (a tab item) is info', () => {
|
|
26
|
-
const f = lintScreen(
|
|
27
|
-
screen([
|
|
28
|
-
{ kind: 'button', testId: 'icon', frame: { x: 0, y: 0, w: 24, h: 24 } },
|
|
29
|
-
{ kind: 'button', testId: 'tab', text: 'HOME', frame: { x: 0, y: 0, w: 26, h: 16 } },
|
|
30
|
-
]),
|
|
31
|
-
);
|
|
32
|
-
const small = f.filter((x) => x.rule === 'small-target');
|
|
33
|
-
assert.equal(small.find((x) => x.message.includes('icon'))?.severity, 'warn');
|
|
34
|
-
assert.equal(small.find((x) => x.message.includes('tab'))?.severity, 'info');
|
|
35
|
-
});
|
|
36
|
-
test('an untranslated i18n key is flagged, real copy is not', () => {
|
|
37
|
-
const f = lintScreen(
|
|
38
|
-
screen([
|
|
39
|
-
{ kind: 'text', text: 'profile.settings.title' },
|
|
40
|
-
{ kind: 'text', text: 'Settings' },
|
|
41
|
-
]),
|
|
42
|
-
);
|
|
43
|
-
assert.equal(f.filter((x) => x.rule === 'untranslated').length, 1);
|
|
44
|
-
});
|
|
45
|
-
test('the theme-[object Object] duplicate testId is caught', () => {
|
|
46
|
-
const f = lintScreen(
|
|
47
|
-
screen([
|
|
48
|
-
{ kind: 'button', testId: 'theme-[object Object]-radio', frame: { x: 0, y: 0, w: 24, h: 24 } },
|
|
49
|
-
{ kind: 'button', testId: 'theme-[object Object]-radio', frame: { x: 0, y: 100, w: 24, h: 24 } },
|
|
50
|
-
]),
|
|
51
|
-
);
|
|
52
|
-
assert.equal(f.filter((x) => x.rule === 'duplicate-testid').length, 1);
|
|
53
|
-
});
|
|
54
|
-
test('a covered interactive element is info, a covered view is nothing', () => {
|
|
55
|
-
const f = lintScreen(
|
|
56
|
-
screen([
|
|
57
|
-
{ kind: 'button', testId: 'under', covered: true },
|
|
58
|
-
{ kind: 'view', covered: true },
|
|
59
|
-
]),
|
|
60
|
-
);
|
|
61
|
-
assert.equal(f.filter((x) => x.rule === 'covered-control').length, 1);
|
|
62
|
-
});
|
|
63
|
-
test('an element drawn past the window is flagged', () => {
|
|
64
|
-
const f = lintScreen(screen([{ kind: 'text', text: 'wide', frame: { x: 300, y: 0, w: 200, h: 20 } }]));
|
|
65
|
-
assert.equal(f.filter((x) => x.rule === 'offscreen').length, 1);
|
|
66
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { summarize, visitsFrom } from '../perf.js';
|
|
5
|
-
import type { TraceEvent } from 'lynkeus-protocol';
|
|
6
|
-
|
|
7
|
-
const ev = (partial: Record<string, unknown>, seq: number): TraceEvent => ({ seq, ...partial }) as unknown as TraceEvent;
|
|
8
|
-
const trace: TraceEvent[] = [
|
|
9
|
-
ev({ kind: 'route', route: 'First.Main', path: ['First.Main'], t: 1000 }, 1),
|
|
10
|
-
ev({ kind: 'request', method: 'GET', url: 'http://x/api/a', status: 200, ms: 120, t: 1130 }, 2),
|
|
11
|
-
ev({ kind: 'request', method: 'GET', url: 'http://x/api/b?limit=100', status: 500, ms: 900, t: 1920 }, 3),
|
|
12
|
-
// Polling long after arrival is not part of what the screen cost to open.
|
|
13
|
-
ev({ kind: 'request', method: 'GET', url: 'http://x/api/a', status: 200, ms: 50, t: 4000 }, 8),
|
|
14
|
-
ev({ kind: 'stall', renders: 50, windowMs: 100, t: 1500 }, 4),
|
|
15
|
-
ev({ kind: 'route', route: 'Second.Main', path: ['Second.Main'], t: 5000 }, 5),
|
|
16
|
-
ev({ kind: 'request', method: 'GET', url: 'http://x/api/c', status: 200, ms: 40, t: 5045 }, 6),
|
|
17
|
-
ev({ kind: 'command', method: 'press', ms: 5, t: 5100 }, 7),
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
test('a visit counts its requests, errors, stalls and when it went quiet', () => {
|
|
21
|
-
const [home, pay] = visitsFrom(trace);
|
|
22
|
-
assert.equal(home!.route, 'First.Main');
|
|
23
|
-
assert.equal(home!.requests, 3);
|
|
24
|
-
assert.equal(home!.errors, 1);
|
|
25
|
-
assert.equal(home!.stalls, 1);
|
|
26
|
-
assert.equal(home!.quietAfterMs, 920); // the 900 ms request ended at +920; the poll at +3950 came after a quiet gap
|
|
27
|
-
assert.deepEqual(home!.slowest, { method: 'GET', path: '/api/b?limit=100', ms: 900 });
|
|
28
|
-
assert.equal(home!.ms, 4000);
|
|
29
|
-
assert.equal(pay!.requests, 1);
|
|
30
|
-
});
|
|
31
|
-
test('the summary averages per route and puts the slowest screen first', () => {
|
|
32
|
-
const s = summarize(visitsFrom(trace));
|
|
33
|
-
assert.deepEqual(
|
|
34
|
-
s.map((r) => r.route),
|
|
35
|
-
['First.Main', 'Second.Main'],
|
|
36
|
-
);
|
|
37
|
-
assert.equal(s[0]!.quietAfterMs, 920);
|
|
38
|
-
});
|
|
39
|
-
test('events before any route are ignored', () => {
|
|
40
|
-
assert.deepEqual(visitsFrom([ev({ kind: 'request', method: 'GET', url: 'http://x/a', ms: 1, t: 1 }, 1)]), []);
|
|
41
|
-
});
|