kryptheon 0.1.10 → 0.1.11

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/bin/kryptheon.js CHANGED
@@ -17,6 +17,8 @@ const replay = require('../kryptheon-replay.js');
17
17
  const selectors = require('../kryptheon-selectors.js');
18
18
  const recordings = require('../kryptheon-recordings.js');
19
19
  const cleanup = require('../kryptheon-cleanup.js');
20
+ const baselines = require('../kryptheon-baselines.js');
21
+ const project = require('../kryptheon-project.js');
20
22
 
21
23
  const PACKAGE_DIR = path.join(__dirname, '..');
22
24
  const USER_DIR = process.cwd();
@@ -1009,6 +1011,39 @@ async function offerToDropLogout(relativeFile) {
1009
1011
  // corrected on the second attempt becomes a wrong login performed on every
1010
1012
  // run. This takes those out and says what it took, because a tool that edits
1011
1013
  // your recording without telling you is worse than one that leaves the mess.
1014
+ // A saved result whose recording is gone, or has been recorded again since,
1015
+ // describes a page nothing here visits any more. Left in place it is handed
1016
+ // to whichever new recording lands on the same name, which then fails on its
1017
+ // first run against a baseline it never created.
1018
+ function dropDeadBaselines() {
1019
+ const api = (function () {
1020
+ try {
1021
+ return require(path.join(PACKAGE_DIR, 'kryptheon-fixture.js'));
1022
+ } catch (err) {
1023
+ return null;
1024
+ }
1025
+ })();
1026
+ if (!api) return [];
1027
+
1028
+ let all;
1029
+ try {
1030
+ all = api.readBaselines(api.BASELINE_FILE);
1031
+ } catch (err) {
1032
+ return [];
1033
+ }
1034
+ if (!all || !Object.keys(all).length) return [];
1035
+
1036
+ const result = baselines.pruneBaselines(all, USER_DIR);
1037
+ if (!result.changed) return [];
1038
+ try {
1039
+ fs.writeFileSync(api.BASELINE_FILE, JSON.stringify(result.baselines, null, 2) + String.fromCharCode(10), "utf8");
1040
+ } catch (err) {
1041
+ return [];
1042
+ }
1043
+ for (const line of baselines.pruneLines(result.dropped)) console.log(line);
1044
+ return result.dropped;
1045
+ }
1046
+
1012
1047
  function tidyRecording(relativeFile) {
1013
1048
  const source = readSpec(relativeFile);
1014
1049
  if (source === null) return [];
@@ -1490,22 +1525,23 @@ function triageSpecs() {
1490
1525
 
1491
1526
  function check(options) {
1492
1527
  const quiet = !!(options && options.quiet);
1528
+
1529
+ // Before anything else. Recordings and results are read out of whatever
1530
+ // folder this was run in, so being in the wrong one does not fail - it
1531
+ // succeeds, about the wrong project. That is the one outcome worth exiting
1532
+ // non-zero over even though nothing was tested.
1533
+ const here = project.inspectProject(USER_DIR);
1534
+ if (!here.ok) {
1535
+ for (const line of project.noProjectLines(here)) console.error(line);
1536
+ return 1;
1537
+ }
1538
+
1493
1539
  // Deliberately not a failure. An assistant told to run this after every
1494
1540
  // change will read a non-zero exit as "something broke" and start fixing
1495
1541
  // code that is perfectly fine. Nothing has been recorded, so there is
1496
1542
  // nothing to verify and nothing to repair.
1497
1543
  if (!listSpecFiles().length) {
1498
- console.log('');
1499
- console.log(' Nothing has been recorded yet, so there is nothing to check.');
1500
- console.log('');
1501
- console.log(' This is not a failure, and there is nothing in the code to fix.');
1502
- console.log('');
1503
- console.log(' To start, record a flow through the app:');
1504
- console.log(' npx kryptheon record <url>');
1505
- console.log('');
1506
- console.log(' That opens a browser window, so it has to be run by a person in');
1507
- console.log(' a normal terminal - it cannot be done from an AI assistant.');
1508
- console.log('');
1544
+ for (const line of project.noRecordingsLines()) console.log(line);
1509
1545
  return 0;
1510
1546
  }
1511
1547
  if (!fixtureResolvesForUser() && specsNeedThePackage()) {
@@ -1522,6 +1558,7 @@ function check(options) {
1522
1558
  return 1;
1523
1559
  }
1524
1560
  keepArtefactsOutOfGit();
1561
+ dropDeadBaselines();
1525
1562
 
1526
1563
  const triage = triageSpecs();
1527
1564
 
@@ -1933,6 +1970,7 @@ module.exports = {
1933
1970
  codegenComplaints: codegenComplaints,
1934
1971
  reportFragileSelectors: reportFragileSelectors,
1935
1972
  tidyRecording: tidyRecording,
1973
+ dropDeadBaselines: dropDeadBaselines,
1936
1974
  reportTidying: reportTidying,
1937
1975
  remove: remove,
1938
1976
  deleteRecording: deleteRecording,
@@ -0,0 +1,176 @@
1
+ // Keeping a saved result tied to the recording it came from.
2
+ //
3
+ // A baseline used to be identified by where the recording lived and what it was
4
+ // called. That is not enough. Delete the spec files with `del tests\*.spec.js` -
5
+ // which is what anyone does before the tool has taught them `kryptheon remove` -
6
+ // record the flow again, and the new recording is handed the same name. Same
7
+ // path, same title, same key. It inherits the old recording's saved page and
8
+ // fails on its very first run, against a baseline it never created and could
9
+ // never have matched.
10
+ //
11
+ // The report that came out of that said both of these at once:
12
+ //
13
+ // This has not passed before.
14
+ // Headings that are gone: "Transactions"
15
+ //
16
+ // Which cannot both be true. If it has never passed there is no baseline of its
17
+ // own to have changed, so there is nothing to show a difference against.
18
+ //
19
+ // So a saved result now also records a fingerprint of the recording that made
20
+ // it. When the recording is gone, or its steps are not the steps the baseline
21
+ // was taken from, the saved result describes something that no longer exists
22
+ // and is thrown away rather than compared against.
23
+ //
24
+ // Pure data work: no Playwright, no test runner.
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const replay = require('./kryptheon-replay.js');
29
+
30
+ /** The spec file half of a baseline key, in one consistent shape. */
31
+ function specOf(key) {
32
+ return String(key || '').split(' :: ')[0].split('\\').join('/');
33
+ }
34
+
35
+ function titleOf(key) {
36
+ const parts = String(key || '').split(' :: ');
37
+ return parts.length > 1 ? parts.slice(1).join(' :: ') : '';
38
+ }
39
+
40
+ /** The fingerprint of the recording in a file, or null if it cannot be read. */
41
+ function fingerprintOf(userDir, specPath) {
42
+ let source;
43
+ try {
44
+ source = fs.readFileSync(path.join(userDir, specPath), 'utf8');
45
+ } catch (e) {
46
+ return null;
47
+ }
48
+ return replay.recordingId(source);
49
+ }
50
+
51
+ /**
52
+ * What each saved result is: still current, orphaned, or stale.
53
+ *
54
+ * `orphan` - the recording it belongs to is not there any more.
55
+ * `stale` - a recording of that name exists, but it is not the one this was
56
+ * taken from. This is the case that cost twenty minutes: the file
57
+ * is present, so nothing looks wrong.
58
+ * `legacy` - written before fingerprints existed. There is no way to tell
59
+ * whether it matches, so it is kept, and a fingerprint is written
60
+ * in so the question can be answered next time. Guessing here would
61
+ * mean either throwing away a good baseline or, far worse, quietly
62
+ * accepting a real regression as the new normal.
63
+ */
64
+ function classifyBaselines(baselines, userDir) {
65
+ const all = baselines || {};
66
+ const verdicts = [];
67
+
68
+ for (const key of Object.keys(all)) {
69
+ const entry = all[key];
70
+ const spec = specOf(key);
71
+ if (!entry || typeof entry !== 'object') {
72
+ verdicts.push({ key: key, state: 'orphan', spec: spec, title: titleOf(key) });
73
+ continue;
74
+ }
75
+
76
+ let exists = false;
77
+ try {
78
+ exists = fs.statSync(path.join(userDir, spec)).isFile();
79
+ } catch (e) {
80
+ exists = false;
81
+ }
82
+ if (!exists) {
83
+ verdicts.push({ key: key, state: 'orphan', spec: spec, title: titleOf(key) });
84
+ continue;
85
+ }
86
+
87
+ const now = fingerprintOf(userDir, spec);
88
+ if (!entry.recordingId) {
89
+ verdicts.push({ key: key, state: 'legacy', spec: spec, title: titleOf(key), fingerprint: now });
90
+ continue;
91
+ }
92
+ if (now && entry.recordingId !== now) {
93
+ verdicts.push({ key: key, state: 'stale', spec: spec, title: titleOf(key), fingerprint: now });
94
+ continue;
95
+ }
96
+ verdicts.push({ key: key, state: 'current', spec: spec, title: titleOf(key) });
97
+ }
98
+
99
+ return verdicts;
100
+ }
101
+
102
+ /**
103
+ * The saved results with the dead ones taken out.
104
+ *
105
+ * Returns a new object; nothing is written here. The caller decides whether the
106
+ * file is worth rewriting, so a check that changes nothing never touches it.
107
+ */
108
+ function pruneBaselines(baselines, userDir) {
109
+ const all = baselines || {};
110
+ const verdicts = classifyBaselines(all, userDir);
111
+ const kept = {};
112
+ const dropped = [];
113
+ let stamped = 0;
114
+
115
+ for (const verdict of verdicts) {
116
+ if (verdict.state === 'orphan' || verdict.state === 'stale') {
117
+ dropped.push(verdict);
118
+ continue;
119
+ }
120
+ const entry = all[verdict.key];
121
+ if (verdict.state === 'legacy' && verdict.fingerprint) {
122
+ // Kept as it is, but tied to this recording from now on.
123
+ kept[verdict.key] = Object.assign({}, entry, { recordingId: verdict.fingerprint });
124
+ stamped++;
125
+ continue;
126
+ }
127
+ kept[verdict.key] = entry;
128
+ }
129
+
130
+ return { baselines: kept, dropped: dropped, stamped: stamped, changed: dropped.length > 0 || stamped > 0 };
131
+ }
132
+
133
+ function plural(n, word) {
134
+ return n + ' ' + word + (n === 1 ? '' : 's');
135
+ }
136
+
137
+ /**
138
+ * One line about what was thrown away, said as a fact rather than a warning.
139
+ *
140
+ * Nothing has gone wrong when this happens - a recording was deleted or redone,
141
+ * which is a normal thing to do. It is said out loud only so that a flow which
142
+ * suddenly starts from scratch is not a mystery.
143
+ */
144
+ function pruneLines(dropped) {
145
+ const list = dropped || [];
146
+ if (!list.length) return [];
147
+
148
+ const orphans = list.filter((d) => d.state === 'orphan');
149
+ const stale = list.filter((d) => d.state === 'stale');
150
+ const lines = [];
151
+
152
+ if (orphans.length) {
153
+ lines.push(
154
+ ' ' + plural(orphans.length, 'old baseline') + ' removed - ' +
155
+ (orphans.length === 1 ? 'its recording is' : 'their recordings are') + ' no longer here.',
156
+ );
157
+ }
158
+ if (stale.length) {
159
+ lines.push(
160
+ ' ' + plural(stale.length, 'old baseline') + ' removed - ' +
161
+ (stale.length === 1 ? 'its recording has' : 'those recordings have') +
162
+ ' been recorded again since.',
163
+ );
164
+ }
165
+ lines.push('');
166
+ return lines;
167
+ }
168
+
169
+ module.exports = {
170
+ specOf: specOf,
171
+ titleOf: titleOf,
172
+ fingerprintOf: fingerprintOf,
173
+ classifyBaselines: classifyBaselines,
174
+ pruneBaselines: pruneBaselines,
175
+ pruneLines: pruneLines,
176
+ };
@@ -20,7 +20,8 @@ const base = require('@playwright/test');
20
20
  // Reuse the reporter's host-stripping so a baseline stores exactly the shape
21
21
  // the reporter prints.
22
22
  const { requestPath } = require('./kryptheon-reporter.js');
23
- const signature = require('./kryptheon-signature.js');
23
+ const signature = require('./kryptheon-signature.js');
24
+ const replay = require('./kryptheon-replay.js');
24
25
 
25
26
  const MAX_ITEMS = 5; // keep the failure block readable
26
27
 
@@ -128,6 +129,10 @@ function normalisedEntry(current) {
128
129
  // Only when there is one. A run whose capture failed must not write an empty
129
130
  // signature over a good one, or the next run would report everything as gone.
130
131
  if (current.signature) stored.signature = current.signature;
132
+ // What recording this was taken from. Without it, a saved result is tied
133
+ // only to a file name, and a re-recording that lands on the same name
134
+ // inherits a baseline it never created and can never match.
135
+ if (current.recordingId) stored.recordingId = current.recordingId;
131
136
  return stored;
132
137
  }
133
138
 
@@ -232,6 +237,17 @@ function newObservations(current, previous) {
232
237
  // Adds a signature to an accepted baseline that predates the feature, without
233
238
  // touching anything else about it - including when it was recorded, which is
234
239
  // what the failure report means by "this last worked at".
240
+ // Ties a saved result written before fingerprints existed to the recording it
241
+ // is now being compared against. Only ever on a pass, so an entry is never
242
+ // bound to a run that did not match it.
243
+ function stampRecording(file, key, recordingId) {
244
+ const all = readBaselines(file);
245
+ const entry = all[key];
246
+ if (!entry || typeof entry !== 'object' || entry.recordingId || !recordingId) return false;
247
+ entry.recordingId = recordingId;
248
+ return saveBaselines(file, all);
249
+ }
250
+
235
251
  function upgradeBaseline(file, key, sig) {
236
252
  const all = readBaselines(file);
237
253
  const entry = all[key];
@@ -247,6 +263,17 @@ function applyBaseline(file, key, current) {
247
263
  writeBaseline(file, key, current);
248
264
  return { status: 'created', message: null };
249
265
  }
266
+
267
+ // A saved result taken from a different recording describes a page this
268
+ // flow never visited. Comparing against it produces a failure on the first
269
+ // run that no amount of fixing the app can clear, so it is replaced rather
270
+ // than argued with. Only when both fingerprints are known: an entry from
271
+ // before they existed is left alone, because "cannot tell" must never
272
+ // become "accept whatever this run saw".
273
+ if (previous.recordingId && current.recordingId && previous.recordingId !== current.recordingId) {
274
+ writeBaseline(file, key, current);
275
+ return { status: 'created', message: null };
276
+ }
250
277
  const message = compareBaselines(previous, current);
251
278
  if (message) return { status: 'changed', message: message };
252
279
 
@@ -257,6 +284,10 @@ function applyBaseline(file, key, current) {
257
284
  upgradeBaseline(file, key, current.signature);
258
285
  return { status: 'learned', message: null };
259
286
  }
287
+ if (!previous.recordingId && current.recordingId) {
288
+ stampRecording(file, key, current.recordingId);
289
+ return { status: 'learned', message: null };
290
+ }
260
291
  return { status: 'match', message: null };
261
292
  }
262
293
 
@@ -283,6 +314,17 @@ const test = base.test.extend({
283
314
 
284
315
  const key = baselineKey(testInfo.file, testInfo.title);
285
316
 
317
+ // The steps this recording actually performs. Comments, blank lines and
318
+ // the test name are ignored, so tidying a file does not read as a
319
+ // different recording - only changing what it does.
320
+ const recordingId = (function () {
321
+ try {
322
+ return replay.recordingId(fs.readFileSync(testInfo.file, 'utf8'));
323
+ } catch (e) {
324
+ return null;
325
+ }
326
+ })();
327
+
286
328
  // Reports only what this run added on top of the last passing run.
287
329
  const attachObservations = async () => {
288
330
  let url = null;
@@ -351,6 +393,7 @@ const test = base.test.extend({
351
393
  consoleErrors: consoleErrors,
352
394
  failedRequests: failedRequests,
353
395
  signature: captured,
396
+ recordingId: recordingId,
354
397
  };
355
398
  } catch (e) {
356
399
  current = null; // page closed by the test - leave any baseline untouched
@@ -384,5 +427,6 @@ module.exports = {
384
427
  compareBaselines: compareBaselines,
385
428
  applyBaseline: applyBaseline,
386
429
  upgradeBaseline: upgradeBaseline,
430
+ stampRecording: stampRecording,
387
431
  BASELINE_FILE: BASELINE_FILE,
388
432
  };
@@ -0,0 +1,199 @@
1
+ // Working out whether this folder is a project at all.
2
+ //
3
+ // `kryptheon check` reads recordings out of ./tests and baselines out of the
4
+ // folder it was run in. Run it somewhere that is not a project - a home folder,
5
+ // the place a global install happened to leave you - and it will happily find
6
+ // whatever tests/ happens to be lying around and run those instead, reporting
7
+ // results for a project nobody asked about. That has already happened to
8
+ // somebody who installed into their home folder.
9
+ //
10
+ // Quietly testing the wrong project is the worst thing this tool can do. A
11
+ // green that belongs to something else is not a result, it is a lie with a tick
12
+ // next to it. So the answer to "is this a project?" has to be settled before
13
+ // anything runs, and the answer has to be a stop rather than a guess.
14
+ //
15
+ // Pure path work: nothing here runs a test or reads a recording.
16
+
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const path = require('path');
20
+
21
+ // For someone who genuinely keeps recordings outside a package. Deliberately
22
+ // long-winded: it should be something you decide to type, not something you
23
+ // stumble into.
24
+ const ESCAPE_HATCH = 'KRYPTHEON_ALLOW_NO_PACKAGE_JSON';
25
+
26
+ function hasPackageJson(dir) {
27
+ try {
28
+ return fs.statSync(path.join(dir, 'package.json')).isFile();
29
+ } catch (e) {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * One comparable form of a path.
36
+ *
37
+ * The trailing separator has to go or C:\Users and C:\Users\ never match, but
38
+ * it must not be taken off before the root test below. "C:" without a separator
39
+ * is not the drive - Node resolves it to the current directory - so a root
40
+ * compared in this form would quietly become "wherever I happen to be".
41
+ */
42
+ function tidy(dir) {
43
+ return path.resolve(String(dir || '')).replace(/[\\/]+$/, '').toLowerCase();
44
+ }
45
+
46
+ /** The folders that are somebody's whole account or the machine itself. */
47
+ function isHomeOrSystem(dir) {
48
+ // A drive or filesystem root: C:\, D:\, /. Done on the resolved path, with
49
+ // its separator still on, for the reason above.
50
+ const resolved = path.resolve(String(dir || ''));
51
+ if (resolved === path.parse(resolved).root) return true;
52
+
53
+ const here = tidy(dir);
54
+ if (here === tidy(os.homedir())) return true;
55
+
56
+ for (const candidate of systemFolders()) {
57
+ if (here === tidy(candidate)) return true;
58
+ }
59
+ return false;
60
+ }
61
+
62
+ /**
63
+ * Every folder that belongs to the machine or to an account rather than to a
64
+ * project, resolved.
65
+ *
66
+ * The first two entries are derived from the running system; the literals after
67
+ * them are fallbacks for the usual layouts. They overlap - on Windows
68
+ * USERPROFILE is the home folder and "/Users" resolves to C:\Users - and the
69
+ * overlap is deliberate: a machine whose home is somewhere unusual is covered
70
+ * by the derived pair, and a machine whose environment is missing entries is
71
+ * covered by the literals.
72
+ */
73
+ function systemFolders() {
74
+ return [
75
+ // The folder every account lives in - C:\Users, /home, /Users. On its own
76
+ // it has no package.json so it would be refused anyway, but one stray npm
77
+ // install there and it would look exactly like a project.
78
+ path.dirname(os.homedir()),
79
+ process.env.SystemRoot,
80
+ process.env.ProgramFiles,
81
+ process.env['ProgramFiles(x86)'],
82
+ process.env.ProgramData,
83
+ process.env.APPDATA,
84
+ process.env.LOCALAPPDATA,
85
+ process.env.USERPROFILE,
86
+ '/root',
87
+ '/home',
88
+ '/Users',
89
+ '/usr',
90
+ '/etc',
91
+ '/bin',
92
+ '/var',
93
+ '/opt',
94
+ ].filter(Boolean);
95
+ }
96
+
97
+ /**
98
+ * Whether this folder can be checked, and if not, why not.
99
+ *
100
+ * `allowed` is the escape hatch having been set. It is reported rather than
101
+ * hidden, so a run that only worked because of it says so.
102
+ */
103
+ function inspectProject(dir, env) {
104
+ const environment = env || process.env;
105
+ const allowed = String(environment[ESCAPE_HATCH] || '') !== '';
106
+ const packaged = hasPackageJson(dir);
107
+ const personal = isHomeOrSystem(dir);
108
+ const base = { dir: dir, packaged: packaged, personal: personal, allowed: allowed };
109
+
110
+ if (allowed) return Object.assign({ ok: true, reason: null }, base);
111
+
112
+ // A home folder is refused even when it has a package.json, and this is not
113
+ // belt and braces - it is the case that actually happened. Someone installed
114
+ // the tool into their home folder, so npm left a package.json and a
115
+ // node_modules there, and a tests folder alongside them. Every sign this
116
+ // could look for says "project"; none of them is true. The measured state of
117
+ // one real machine: C:\Users\<name> holding package.json, node_modules and
118
+ // tests, none of which belong to anything.
119
+ if (personal) return Object.assign({ ok: false, reason: 'personal' }, base);
120
+ if (!packaged) return Object.assign({ ok: false, reason: 'no-package' }, base);
121
+ return Object.assign({ ok: true, reason: null }, base);
122
+ }
123
+
124
+ /**
125
+ * Why this folder is not a project, and what to do instead.
126
+ *
127
+ * The folder is printed back because the commonest version of this is not
128
+ * knowing where you are - a new terminal, a global install, a shell that opened
129
+ * somewhere else.
130
+ */
131
+ function noProjectLines(inspection) {
132
+ const lines = [''];
133
+
134
+ if (inspection.personal) {
135
+ lines.push(' This is your home or a system folder, not a project.');
136
+ lines.push('');
137
+ lines.push(' You are in: ' + inspection.dir);
138
+ lines.push('');
139
+ // Almost never deliberate, and the one place where running anyway does the
140
+ // most damage. Said explicitly because a home folder that has had anything
141
+ // installed into it looks exactly like a project from the outside.
142
+ lines.push(' Recordings and results are kept beside the app they belong to,');
143
+ lines.push(' so running here would check whichever tests folder happens to be');
144
+ lines.push(' sitting in this one - almost certainly not the one you meant.');
145
+ if (inspection.packaged) {
146
+ lines.push('');
147
+ // Said plainly, because otherwise the package.json sitting right there
148
+ // makes this message look like the bug rather than the diagnosis.
149
+ lines.push(' There is a package.json here, but that is not what makes a project.');
150
+ lines.push(' It looks like npm install was run in this folder by mistake, which');
151
+ lines.push(' leaves a package.json and a node_modules behind.');
152
+ }
153
+ lines.push('');
154
+ } else {
155
+ lines.push(' There is no project here - this folder has no package.json.');
156
+ lines.push('');
157
+ lines.push(' You are in: ' + inspection.dir);
158
+ lines.push('');
159
+ }
160
+
161
+ lines.push(' Change into your project folder and run this again:');
162
+ lines.push('');
163
+ lines.push(' cd path\\to\\your\\project');
164
+ lines.push(' kryptheon check');
165
+ lines.push('');
166
+ lines.push(' If you really do keep recordings outside a package, set');
167
+ lines.push(' ' + ESCAPE_HATCH + '=1 and this will run anyway.');
168
+ lines.push('');
169
+ return lines;
170
+ }
171
+
172
+ /** A real project, with nothing recorded in it yet. */
173
+ function noRecordingsLines() {
174
+ return [
175
+ '',
176
+ ' No recordings were found in this project.',
177
+ '',
178
+ ' This is not a failure, and there is nothing in the code to fix.',
179
+ '',
180
+ ' Record a flow through your app first:',
181
+ '',
182
+ ' kryptheon record http://localhost:3000',
183
+ '',
184
+ ' That opens a browser window, so it has to be run by a person in a',
185
+ ' normal terminal - it cannot be done from an AI assistant.',
186
+ '',
187
+ ];
188
+ }
189
+
190
+ module.exports = {
191
+ ESCAPE_HATCH: ESCAPE_HATCH,
192
+ hasPackageJson: hasPackageJson,
193
+ systemFolders: systemFolders,
194
+ tidy: tidy,
195
+ isHomeOrSystem: isHomeOrSystem,
196
+ inspectProject: inspectProject,
197
+ noProjectLines: noProjectLines,
198
+ noRecordingsLines: noRecordingsLines,
199
+ };
@@ -726,7 +726,14 @@ class KryptheonReporter {
726
726
  out.push('X ' + test.title);
727
727
  out.push(' ' + translated.reason);
728
728
 
729
- for (const detail of translated.details || []) out.push(' ' + detail);
729
+ // A difference against a baseline only means something if there is a
730
+ // baseline of this recording's own to differ from. Saying "this has not
731
+ // passed before" and then listing headings that are gone says two things
732
+ // that cannot both be true, and the reader spends their time trying to
733
+ // reconcile them instead of reading the actual failure.
734
+ const aboutBaseline = /Baseline changed|ended up somewhere different/.test(translated.reason || '');
735
+ const details = lastPass || !aboutBaseline ? translated.details || [] : [];
736
+ for (const detail of details) out.push(' ' + detail);
730
737
 
731
738
  out.push(lastPass ? ' This was working on ' + formatWhen(lastPass) + '.' : ' This has not passed before.');
732
739
 
package/package.json CHANGED
@@ -1,67 +1,71 @@
1
- {
2
- "name": "kryptheon",
3
- "version": "0.1.10",
4
- "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
- "license": "MIT",
6
- "type": "commonjs",
7
- "bin": {
8
- "kryptheon": "bin/kryptheon.js"
9
- },
10
- "exports": {
11
- ".": "./kryptheon-fixture.js",
12
- "./kryptheon-fixture": "./kryptheon-fixture.js",
13
- "./kryptheon-reporter": "./kryptheon-reporter.js",
14
- "./package.json": "./package.json",
15
- "./kryptheon-replay": "./kryptheon-replay.js",
16
- "./kryptheon-signature": "./kryptheon-signature.js",
17
- "./kryptheon-secrets": "./kryptheon-secrets.js"
18
- },
19
- "files": [
20
- "bin/",
21
- "kryptheon-fixture.js",
22
- "kryptheon-reporter.js",
23
- "kryptheon-signature.js",
24
- "kryptheon-secrets.js",
25
- "playwright.config.js",
26
- "README.md",
27
- "kryptheon-replay.js",
28
- "kryptheon-selectors.js",
29
- "kryptheon-recordings.js",
30
- "kryptheon-cleanup.js"
31
- ],
32
- "engines": {
33
- "node": ">=20.6.0"
34
- },
35
- "dependencies": {
36
- "@playwright/test": "^1.62.1"
37
- },
38
- "scripts": {
39
- "check": "node kryptheon-reporter.check.js && node kryptheon-signature.check.js && node kryptheon-signature.stream.check.js && node kryptheon-signature.baseline.check.js && node kryptheon-signature.settle.check.js && node kryptheon-signature.number.check.js && node kryptheon-step3.check.js && node kryptheon-watchdog.check.js && node kryptheon-record.check.js && node kryptheon-selectors.check.js && node kryptheon-recordings.check.js && node kryptheon-cleanup.check.js && node kryptheon-package.check.js",
40
- "check:stream": "node kryptheon-signature.stream.check.js",
41
- "check:baseline": "node kryptheon-signature.baseline.check.js",
42
- "check:settle": "node kryptheon-signature.settle.check.js",
43
- "check:step3": "node kryptheon-step3.check.js",
44
- "check:watchdog": "node kryptheon-watchdog.check.js",
45
- "check:record": "node kryptheon-record.check.js",
46
- "check:package": "node kryptheon-package.check.js",
47
- "prepublishOnly": "node kryptheon-package.check.js",
48
- "check:selectors": "node kryptheon-selectors.check.js",
49
- "check:number": "node kryptheon-signature.number.check.js",
50
- "check:recordings": "node kryptheon-recordings.check.js",
51
- "check:cleanup": "node kryptheon-cleanup.check.js"
52
- },
53
- "keywords": [
54
- "playwright",
55
- "testing",
56
- "end-to-end",
57
- "cli"
58
- ],
59
- "repository": {
60
- "type": "git",
61
- "url": "git+https://github.com/jhashantanu987-beep/kryptheon-cli.git"
62
- },
63
- "bugs": {
64
- "url": "https://github.com/jhashantanu987-beep/kryptheon-cli/issues"
65
- },
66
- "homepage": "https://github.com/jhashantanu987-beep/kryptheon-cli#readme"
67
- }
1
+ {
2
+ "name": "kryptheon",
3
+ "version": "0.1.11",
4
+ "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "kryptheon": "bin/kryptheon.js"
9
+ },
10
+ "exports": {
11
+ ".": "./kryptheon-fixture.js",
12
+ "./kryptheon-fixture": "./kryptheon-fixture.js",
13
+ "./kryptheon-reporter": "./kryptheon-reporter.js",
14
+ "./package.json": "./package.json",
15
+ "./kryptheon-replay": "./kryptheon-replay.js",
16
+ "./kryptheon-signature": "./kryptheon-signature.js",
17
+ "./kryptheon-secrets": "./kryptheon-secrets.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "kryptheon-fixture.js",
22
+ "kryptheon-reporter.js",
23
+ "kryptheon-signature.js",
24
+ "kryptheon-secrets.js",
25
+ "playwright.config.js",
26
+ "README.md",
27
+ "kryptheon-replay.js",
28
+ "kryptheon-selectors.js",
29
+ "kryptheon-recordings.js",
30
+ "kryptheon-cleanup.js",
31
+ "kryptheon-baselines.js",
32
+ "kryptheon-project.js"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20.6.0"
36
+ },
37
+ "dependencies": {
38
+ "@playwright/test": "^1.62.1"
39
+ },
40
+ "scripts": {
41
+ "check": "node kryptheon-reporter.check.js && node kryptheon-signature.check.js && node kryptheon-signature.stream.check.js && node kryptheon-signature.baseline.check.js && node kryptheon-signature.settle.check.js && node kryptheon-signature.number.check.js && node kryptheon-step3.check.js && node kryptheon-watchdog.check.js && node kryptheon-record.check.js && node kryptheon-selectors.check.js && node kryptheon-recordings.check.js && node kryptheon-cleanup.check.js && node kryptheon-orphan.check.js && node kryptheon-project.check.js && node kryptheon-package.check.js",
42
+ "check:stream": "node kryptheon-signature.stream.check.js",
43
+ "check:baseline": "node kryptheon-signature.baseline.check.js",
44
+ "check:settle": "node kryptheon-signature.settle.check.js",
45
+ "check:step3": "node kryptheon-step3.check.js",
46
+ "check:watchdog": "node kryptheon-watchdog.check.js",
47
+ "check:record": "node kryptheon-record.check.js",
48
+ "check:package": "node kryptheon-package.check.js",
49
+ "prepublishOnly": "node kryptheon-package.check.js",
50
+ "check:selectors": "node kryptheon-selectors.check.js",
51
+ "check:number": "node kryptheon-signature.number.check.js",
52
+ "check:recordings": "node kryptheon-recordings.check.js",
53
+ "check:cleanup": "node kryptheon-cleanup.check.js",
54
+ "check:orphan": "node kryptheon-orphan.check.js",
55
+ "check:project": "node kryptheon-project.check.js"
56
+ },
57
+ "keywords": [
58
+ "playwright",
59
+ "testing",
60
+ "end-to-end",
61
+ "cli"
62
+ ],
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/jhashantanu987-beep/kryptheon-cli.git"
66
+ },
67
+ "bugs": {
68
+ "url": "https://github.com/jhashantanu987-beep/kryptheon-cli/issues"
69
+ },
70
+ "homepage": "https://github.com/jhashantanu987-beep/kryptheon-cli#readme"
71
+ }