kryptheon 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -21
- package/bin/kryptheon.js +146 -3
- package/kryptheon-replay.js +0 -0
- package/kryptheon-reporter.js +114 -9
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
# kryptheon
|
|
2
2
|
|
|
3
|
-
Record
|
|
3
|
+
Record a flow in your app once, replay it after every AI change, and get told in
|
|
4
|
+
plain English what broke.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Runs entirely on your machine. No account, no API key, nothing leaves your
|
|
7
|
+
computer. It is a wrapper around [Playwright](https://playwright.dev).
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
Made by a 17-year-old vibecoder who kept having AI break things in his own app.
|
|
8
10
|
|
|
9
|
-
Install
|
|
11
|
+
## Install
|
|
10
12
|
|
|
11
13
|
```
|
|
12
14
|
npm i -D kryptheon
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
`
|
|
17
|
-
|
|
18
|
-
Running `npx kryptheon` without installing will record fine but fail to check.
|
|
17
|
+
Install it into your project, not globally — every recording imports
|
|
18
|
+
`kryptheon/kryptheon-fixture`, and your tests can only find that if kryptheon is
|
|
19
|
+
in your project's `node_modules`.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
## Record
|
|
21
22
|
|
|
22
23
|
```
|
|
23
24
|
npx kryptheon record https://your-app.example.com
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
A browser opens. Use your app the way a customer would
|
|
27
|
-
|
|
27
|
+
A browser opens. Use your app the way a customer would. Close the browser when
|
|
28
|
+
you are done, and the recording is saved as a test.
|
|
29
|
+
|
|
30
|
+
## Check
|
|
28
31
|
|
|
29
32
|
```
|
|
30
33
|
npx kryptheon check
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
Runs everything you have recorded
|
|
36
|
+
Runs everything you have recorded:
|
|
34
37
|
|
|
35
38
|
```
|
|
36
39
|
OK Sign in (4.1s)
|
|
@@ -45,15 +48,28 @@ X Checkout
|
|
|
45
48
|
the server crashed or is unavailable - the error is in backend code, not the page
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
Every failure
|
|
49
|
-
|
|
51
|
+
Every failure ends with a short summary you can paste straight into an AI coding
|
|
52
|
+
tool.
|
|
53
|
+
|
|
54
|
+
## Recording something that replays
|
|
55
|
+
|
|
56
|
+
A recording is only useful if it still works the second time. So:
|
|
57
|
+
|
|
58
|
+
- **Record one simple flow.** Sign in and look at a page. Open a thing and check
|
|
59
|
+
it loaded. Short beats thorough.
|
|
60
|
+
- **Sign in with an account that already exists.** Do not sign up during a
|
|
61
|
+
recording — the account exists next time, and the signup step fails.
|
|
62
|
+
- **Avoid steps that cannot repeat.** Adding data piles up a row on every run.
|
|
63
|
+
Logging out at the end means the next run does not start where this one did.
|
|
64
|
+
|
|
65
|
+
Kryptheon warns you about these after a recording, and names the exact step.
|
|
50
66
|
|
|
51
67
|
## Automatic checks
|
|
52
68
|
|
|
53
|
-
The first time a test passes, kryptheon remembers where the browser ended up
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
The first time a test passes, kryptheon remembers where the browser ended up and
|
|
70
|
+
what the page was called. If either changes later, the test fails and says what
|
|
71
|
+
changed — so a recording catches regressions without you writing a single
|
|
72
|
+
assertion.
|
|
57
73
|
|
|
58
74
|
When a change is intentional, accept it for that one test:
|
|
59
75
|
|
|
@@ -71,15 +87,18 @@ npx kryptheon accept "Checkout"
|
|
|
71
87
|
| `kryptheon-baselines.json` | the remembered result for each test |
|
|
72
88
|
| `kryptheon-history.jsonl` | one line per run, used for "this was working on …" |
|
|
73
89
|
|
|
74
|
-
`.env` in the same folder is loaded automatically,
|
|
75
|
-
|
|
90
|
+
A `.env` in the same folder is loaded automatically, and anything you type into a
|
|
91
|
+
password box is replaced with an environment variable rather than written into
|
|
92
|
+
the test.
|
|
76
93
|
|
|
77
94
|
Worth adding to `.gitignore`: `.env`, `kryptheon-baselines.json`,
|
|
78
95
|
`kryptheon-history.jsonl`, `test-results/`.
|
|
79
96
|
|
|
80
97
|
## Requirements
|
|
81
98
|
|
|
82
|
-
Node 20.6 or later.
|
|
99
|
+
Node 20.6 or later. Recording needs a real terminal window — it opens a browser,
|
|
100
|
+
so it cannot run inside an AI coding assistant. The first run downloads a browser
|
|
101
|
+
(about 200MB, once).
|
|
83
102
|
|
|
84
103
|
## Licence
|
|
85
104
|
|
package/bin/kryptheon.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { spawn, spawnSync } = require('child_process');
|
|
14
|
+
const replay = require('../kryptheon-replay.js');
|
|
14
15
|
|
|
15
16
|
const PACKAGE_DIR = path.join(__dirname, '..');
|
|
16
17
|
const USER_DIR = process.cwd();
|
|
@@ -404,8 +405,13 @@ function deriveTestName(source) {
|
|
|
404
405
|
if (name) return name;
|
|
405
406
|
}
|
|
406
407
|
|
|
407
|
-
|
|
408
|
-
|
|
408
|
+
// The first thing done describes a recording better than the last, and the
|
|
409
|
+
// last is often "Sign Out", which would name every recording after the way
|
|
410
|
+
// it ended rather than what it was for.
|
|
411
|
+
const names = [...String(source).matchAll(/name:\s*['"]([^'"]+)['"]/g)]
|
|
412
|
+
.map((m) => m[1].trim())
|
|
413
|
+
.filter((name) => name && !/(log|sign)[\s_-]?out/i.test(name));
|
|
414
|
+
if (names.length) return names[0];
|
|
409
415
|
|
|
410
416
|
const hosts = [...String(source).matchAll(/page\.goto\(\s*['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
411
417
|
if (hosts.length) {
|
|
@@ -529,6 +535,111 @@ function startCodegen(args) {
|
|
|
529
535
|
});
|
|
530
536
|
}
|
|
531
537
|
|
|
538
|
+
// --- making a recording fit to run twice ------------------------------------
|
|
539
|
+
|
|
540
|
+
function readSpec(relativeFile) {
|
|
541
|
+
try {
|
|
542
|
+
return fs.readFileSync(path.join(USER_DIR, relativeFile), 'utf8');
|
|
543
|
+
} catch (err) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function writeSpec(relativeFile, source) {
|
|
549
|
+
try {
|
|
550
|
+
fs.writeFileSync(path.join(USER_DIR, relativeFile), source, 'utf8');
|
|
551
|
+
return true;
|
|
552
|
+
} catch (err) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Passwords typed during recording land in the file as plain text. Swap them
|
|
558
|
+
// for environment variables before the file is left lying around.
|
|
559
|
+
function takeOutSecrets(relativeFile) {
|
|
560
|
+
const source = readSpec(relativeFile);
|
|
561
|
+
if (source === null) return [];
|
|
562
|
+
const result = replay.scrubSecrets(source);
|
|
563
|
+
if (!result.replacements.length) return [];
|
|
564
|
+
return writeSpec(relativeFile, result.source) ? result.replacements : [];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function reportSecrets(replacements) {
|
|
568
|
+
if (!replacements || !replacements.length) return;
|
|
569
|
+
console.log(' What you typed into the password box was not saved in the test.');
|
|
570
|
+
console.log('');
|
|
571
|
+
console.log(' Put it in a file called .env next to your tests, like this:');
|
|
572
|
+
for (const item of replacements) {
|
|
573
|
+
console.log(' ' + item.envName + '=your ' + String(item.field).toLowerCase() + ' here');
|
|
574
|
+
}
|
|
575
|
+
console.log('');
|
|
576
|
+
console.log(' Keep .env out of version control - it holds the real value.');
|
|
577
|
+
console.log('');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function askYesNo(question) {
|
|
581
|
+
return new Promise((resolve) => {
|
|
582
|
+
if (!process.stdin.isTTY) return resolve(false);
|
|
583
|
+
const readline = require('readline');
|
|
584
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
585
|
+
rl.question(question, (answer) => {
|
|
586
|
+
rl.close();
|
|
587
|
+
// Let the loop finish later: stdin stays open otherwise.
|
|
588
|
+
try {
|
|
589
|
+
process.stdin.pause();
|
|
590
|
+
} catch (err) {
|
|
591
|
+
/* nothing to do */
|
|
592
|
+
}
|
|
593
|
+
resolve(!/^\s*n/i.test(String(answer)));
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Ending on a logout means the next run starts somewhere else. Almost nobody
|
|
599
|
+
// wants that, so ask.
|
|
600
|
+
async function offerToDropLogout(relativeFile) {
|
|
601
|
+
const source = readSpec(relativeFile);
|
|
602
|
+
if (source === null) return false;
|
|
603
|
+
const trimmed = replay.dropTrailingLogout(source);
|
|
604
|
+
if (!trimmed.removed) return false;
|
|
605
|
+
|
|
606
|
+
console.log(' The recording ends by logging out ("' + trimmed.removed + '").');
|
|
607
|
+
console.log(' That means the next run would not start where this one did.');
|
|
608
|
+
console.log('');
|
|
609
|
+
|
|
610
|
+
const drop = await askYesNo(' Leave that last step out? [Y/n] ');
|
|
611
|
+
if (!drop) {
|
|
612
|
+
console.log('');
|
|
613
|
+
console.log(' Kept it. If checks fail from the second run onwards, this is why.');
|
|
614
|
+
console.log('');
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
writeSpec(relativeFile, trimmed.source);
|
|
618
|
+
console.log('');
|
|
619
|
+
console.log(' Removed the sign-out step.');
|
|
620
|
+
console.log('');
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function reportReplayRisks(relativeFile) {
|
|
625
|
+
const source = readSpec(relativeFile);
|
|
626
|
+
if (source === null) return [];
|
|
627
|
+
const risks = replay.findReplayRisks(source);
|
|
628
|
+
if (!risks.length) return [];
|
|
629
|
+
|
|
630
|
+
console.log(' Before you rely on this recording, one thing to know:');
|
|
631
|
+
console.log('');
|
|
632
|
+
for (const risk of risks) {
|
|
633
|
+
console.log(' "' + risk.step + '" (line ' + risk.line + ')');
|
|
634
|
+
console.log(' ' + risk.why + '.');
|
|
635
|
+
console.log(' What to do: ' + risk.fix + '.');
|
|
636
|
+
console.log('');
|
|
637
|
+
}
|
|
638
|
+
console.log(' Everything else in the recording will run again fine.');
|
|
639
|
+
console.log('');
|
|
640
|
+
return risks;
|
|
641
|
+
}
|
|
642
|
+
|
|
532
643
|
// Everything that has to happen to whatever codegen left behind, however
|
|
533
644
|
// the run ended. Module level so it can be exercised directly.
|
|
534
645
|
async function finaliseRecording(outFile, context) {
|
|
@@ -545,11 +656,21 @@ async function finaliseRecording(outFile, context) {
|
|
|
545
656
|
// Import rewrite first: it is instant, so even a second interruption
|
|
546
657
|
// leaves behind a spec that works.
|
|
547
658
|
pointAtFixture(outFile);
|
|
659
|
+
|
|
660
|
+
// Anything typed into a password box is still a literal in this file at
|
|
661
|
+
// this point. Take it out before doing anything else with the file.
|
|
662
|
+
const secrets = takeOutSecrets(outFile);
|
|
663
|
+
|
|
548
664
|
const named = await nameRecording(outFile);
|
|
549
665
|
console.log('');
|
|
550
666
|
console.log(' Saved to ' + named);
|
|
551
667
|
console.log(' Run it any time with: kryptheon check');
|
|
552
668
|
console.log('');
|
|
669
|
+
|
|
670
|
+
reportSecrets(secrets);
|
|
671
|
+
await offerToDropLogout(named);
|
|
672
|
+
reportReplayRisks(named);
|
|
673
|
+
|
|
553
674
|
return { code: 0, savedAs: named };
|
|
554
675
|
}
|
|
555
676
|
|
|
@@ -746,6 +867,7 @@ function triageSpecs() {
|
|
|
746
867
|
const repaired = [];
|
|
747
868
|
const notRecordings = [];
|
|
748
869
|
const runnable = [];
|
|
870
|
+
const withSecrets = [];
|
|
749
871
|
|
|
750
872
|
for (const name of listSpecFiles()) {
|
|
751
873
|
const full = path.join(TESTS_DIR, name);
|
|
@@ -772,10 +894,21 @@ function triageSpecs() {
|
|
|
772
894
|
notRecordings.push(name);
|
|
773
895
|
continue;
|
|
774
896
|
}
|
|
897
|
+
// Recordings made before secrets were stripped still hold the real value.
|
|
898
|
+
// Say so rather than rewriting: a spec that works today should keep
|
|
899
|
+
// working until the person has somewhere to put the password.
|
|
900
|
+
if (replay.scrubSecrets(source).replacements.length) {
|
|
901
|
+
withSecrets.push({ name: name, fields: replay.scrubSecrets(source).replacements });
|
|
902
|
+
}
|
|
775
903
|
runnable.push(name);
|
|
776
904
|
}
|
|
777
905
|
|
|
778
|
-
return {
|
|
906
|
+
return {
|
|
907
|
+
repaired: repaired,
|
|
908
|
+
notRecordings: notRecordings,
|
|
909
|
+
runnable: runnable,
|
|
910
|
+
withSecrets: withSecrets,
|
|
911
|
+
};
|
|
779
912
|
}
|
|
780
913
|
|
|
781
914
|
function check() {
|
|
@@ -811,6 +944,13 @@ function check() {
|
|
|
811
944
|
console.log(' It was saved without the part that explains failures. Fixed now.');
|
|
812
945
|
}
|
|
813
946
|
|
|
947
|
+
for (const item of triage.withSecrets) {
|
|
948
|
+
console.log('');
|
|
949
|
+
console.log(' Careful: ' + path.join('tests', item.name) + ' has a real password written into it.');
|
|
950
|
+
console.log(' Anyone who can read that file can read the password.');
|
|
951
|
+
console.log(' Re-record it to have the password moved into .env automatically.');
|
|
952
|
+
}
|
|
953
|
+
|
|
814
954
|
for (const name of triage.notRecordings) {
|
|
815
955
|
console.log('');
|
|
816
956
|
console.log(' Skipped ' + path.join('tests', name));
|
|
@@ -1026,6 +1166,9 @@ module.exports = {
|
|
|
1026
1166
|
triageSpecs: triageSpecs,
|
|
1027
1167
|
record: record,
|
|
1028
1168
|
finaliseRecording: finaliseRecording,
|
|
1169
|
+
takeOutSecrets: takeOutSecrets,
|
|
1170
|
+
reportReplayRisks: reportReplayRisks,
|
|
1171
|
+
offerToDropLogout: offerToDropLogout,
|
|
1029
1172
|
looksLikeNoDesktop: looksLikeNoDesktop,
|
|
1030
1173
|
classifyFetchError: classifyFetchError,
|
|
1031
1174
|
normaliseRecordUrl: normaliseRecordUrl,
|
|
Binary file
|
package/kryptheon-reporter.js
CHANGED
|
@@ -477,6 +477,84 @@ function buildPromptLines(testTitle, translated, obs, lastPassIso, now) {
|
|
|
477
477
|
);
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// The first run is a special case: nothing has regressed, because nothing has
|
|
482
|
+
// ever passed. Usually the recording itself contains a step that only worked
|
|
483
|
+
// the once.
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
// Playwright works the error's file out of a stack trace, and its parser
|
|
487
|
+
// stops at the first "(" - so a folder called "New folder (2)" arrives as
|
|
488
|
+
// "2)\tests\thing.spec.js". The path declared for the test itself is read from
|
|
489
|
+
// the file system and is always whole, so prefer that whenever the stack's
|
|
490
|
+
// version does not point at a real file.
|
|
491
|
+
function trustworthySpecPath(test, errorFile) {
|
|
492
|
+
const declared = (test && test.location && test.location.file) || null;
|
|
493
|
+
if (errorFile && path.isAbsolute(errorFile)) {
|
|
494
|
+
try {
|
|
495
|
+
if (fs.existsSync(errorFile)) return errorFile;
|
|
496
|
+
} catch (err) {
|
|
497
|
+
/* fall through to the declared path */
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (declared) {
|
|
501
|
+
try {
|
|
502
|
+
if (fs.existsSync(declared)) return declared;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
/* fall through */
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return errorFile || declared || null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Identity of the recording itself, so history cannot be inherited by a
|
|
511
|
+
// different recording that happens to share a name.
|
|
512
|
+
function recordingIdFor(specPath) {
|
|
513
|
+
if (!specPath) return null;
|
|
514
|
+
let source;
|
|
515
|
+
try {
|
|
516
|
+
source = fs.readFileSync(specPath, 'utf8');
|
|
517
|
+
} catch (err) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
return require('./kryptheon-replay.js').recordingId(source);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function firstRunAdvice(test) {
|
|
528
|
+
const lines = [];
|
|
529
|
+
lines.push(' This is the first run of this recording, so nothing has broken yet -');
|
|
530
|
+
lines.push(' a step that cannot happen twice is the more likely explanation.');
|
|
531
|
+
|
|
532
|
+
const file = test && test.location && test.location.file;
|
|
533
|
+
if (!file) return { lines: lines, namedAStep: false };
|
|
534
|
+
|
|
535
|
+
let source;
|
|
536
|
+
try {
|
|
537
|
+
source = fs.readFileSync(file, 'utf8');
|
|
538
|
+
} catch (err) {
|
|
539
|
+
return { lines: lines, namedAStep: false };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
let risks = [];
|
|
543
|
+
try {
|
|
544
|
+
risks = require('./kryptheon-replay.js').findReplayRisks(source);
|
|
545
|
+
} catch (err) {
|
|
546
|
+
return { lines: lines, namedAStep: false };
|
|
547
|
+
}
|
|
548
|
+
if (!risks.length) return { lines: lines, namedAStep: false };
|
|
549
|
+
|
|
550
|
+
lines.push(' Most likely the step that cannot repeat:');
|
|
551
|
+
for (const risk of risks.slice(0, 3)) {
|
|
552
|
+
lines.push(' - "' + risk.step + '" (line ' + risk.line + ') - ' + risk.why + '.');
|
|
553
|
+
}
|
|
554
|
+
lines.push(' Record again without it, and this should settle down.');
|
|
555
|
+
return { lines: lines, namedAStep: true };
|
|
556
|
+
}
|
|
557
|
+
|
|
480
558
|
// ---------------------------------------------------------------------------
|
|
481
559
|
// Reporter
|
|
482
560
|
// ---------------------------------------------------------------------------
|
|
@@ -518,15 +596,22 @@ class KryptheonReporter {
|
|
|
518
596
|
}
|
|
519
597
|
}
|
|
520
598
|
|
|
521
|
-
// Most recent earlier run in which
|
|
522
|
-
|
|
599
|
+
// Most recent earlier run in which THIS recording passed. The name alone is
|
|
600
|
+
// not enough: deleting a recording and making a new one that happens to end
|
|
601
|
+
// up with the same name would otherwise inherit the old one's history, and
|
|
602
|
+
// a brand new recording would be told it "was working" yesterday.
|
|
603
|
+
_lastPassed(title, recordingId) {
|
|
523
604
|
for (let i = this.previousRuns.length - 1; i >= 0; i--) {
|
|
524
605
|
const run = this.previousRuns[i];
|
|
525
606
|
const tests = (run && run.tests) || [];
|
|
526
607
|
for (let j = tests.length - 1; j >= 0; j--) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
608
|
+
const entry = tests[j];
|
|
609
|
+
if (!entry || entry.title !== title || entry.status !== 'passed') continue;
|
|
610
|
+
// Entries written before recordings were fingerprinted cannot be
|
|
611
|
+
// matched to one, so they are not claimed by any.
|
|
612
|
+
if (!entry.recordingId || !recordingId) continue;
|
|
613
|
+
if (entry.recordingId !== recordingId) continue;
|
|
614
|
+
return entry.timestamp || run.runAt;
|
|
530
615
|
}
|
|
531
616
|
}
|
|
532
617
|
return null;
|
|
@@ -537,11 +622,16 @@ class KryptheonReporter {
|
|
|
537
622
|
result.status === 'passed' ? 'passed' : result.status === 'skipped' ? 'skipped' : 'failed';
|
|
538
623
|
const timestamp = (result.startTime instanceof Date ? result.startTime : new Date()).toISOString();
|
|
539
624
|
|
|
625
|
+
const specPath = trustworthySpecPath(test, null);
|
|
626
|
+
const recordingId = recordingIdFor(specPath);
|
|
627
|
+
|
|
540
628
|
const record = {
|
|
541
629
|
title: test.title,
|
|
542
630
|
status: status,
|
|
543
631
|
durationMs: result.duration,
|
|
544
632
|
timestamp: timestamp,
|
|
633
|
+
recordingId: recordingId,
|
|
634
|
+
specFile: specPath ? path.relative(USER_DIR, specPath) : null,
|
|
545
635
|
};
|
|
546
636
|
|
|
547
637
|
if (status === 'skipped') {
|
|
@@ -585,7 +675,7 @@ class KryptheonReporter {
|
|
|
585
675
|
const observations = readObservations(result.attachments);
|
|
586
676
|
const located = primary.location || (errors.find(function (e) { return e.location; }) || {}).location;
|
|
587
677
|
const line = located ? located.line : null;
|
|
588
|
-
const file = located ? located.file : null;
|
|
678
|
+
const file = trustworthySpecPath(test, located ? located.file : null);
|
|
589
679
|
const locator = extractLocator(message);
|
|
590
680
|
const translated = translate(message);
|
|
591
681
|
|
|
@@ -612,13 +702,28 @@ class KryptheonReporter {
|
|
|
612
702
|
|
|
613
703
|
for (const detail of translated.details || []) out.push(' ' + detail);
|
|
614
704
|
|
|
615
|
-
const lastPass = this._lastPassed(test.title);
|
|
705
|
+
const lastPass = this._lastPassed(test.title, recordingId);
|
|
616
706
|
out.push(lastPass ? ' This was working on ' + formatWhen(lastPass) + '.' : ' This has not passed before.');
|
|
617
707
|
|
|
618
|
-
|
|
708
|
+
// A first run that fails is a different situation from a regression:
|
|
709
|
+
// nothing has broken, because nothing ever worked. Far more often the
|
|
710
|
+
// recording contains a step that cannot happen twice.
|
|
711
|
+
let blamedTheRecording = false;
|
|
712
|
+
if (!lastPass) {
|
|
713
|
+
const advice = firstRunAdvice(test);
|
|
714
|
+
for (const line of advice.lines) out.push(line);
|
|
715
|
+
blamedTheRecording = advice.namedAStep;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// On a first run we have already named the step that cannot repeat, and
|
|
719
|
+
// the usual "your last change may have..." advice would contradict it.
|
|
720
|
+
if (blamedTheRecording) {
|
|
721
|
+
/* the first-run block above already says what to do */
|
|
722
|
+
} else if (translated.advice) {
|
|
619
723
|
out.push(' What to check: ' + translated.advice.split('%TEST%').join(test.title));
|
|
724
|
+
} else {
|
|
725
|
+
out.push(' What to check: this is the raw message from the test tool - it was not recognised.');
|
|
620
726
|
}
|
|
621
|
-
else out.push(' What to check: this is the raw message from the test tool - it was not recognised.');
|
|
622
727
|
|
|
623
728
|
for (const observed of whereToLookLines(observations)) out.push(observed);
|
|
624
729
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kryptheon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Record what you do in your app and get plain-language reports when it breaks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -11,14 +11,16 @@
|
|
|
11
11
|
".": "./kryptheon-fixture.js",
|
|
12
12
|
"./kryptheon-fixture": "./kryptheon-fixture.js",
|
|
13
13
|
"./kryptheon-reporter": "./kryptheon-reporter.js",
|
|
14
|
-
"./package.json": "./package.json"
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./kryptheon-replay": "./kryptheon-replay.js"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"bin/",
|
|
18
19
|
"kryptheon-fixture.js",
|
|
19
20
|
"kryptheon-reporter.js",
|
|
20
21
|
"playwright.config.js",
|
|
21
|
-
"README.md"
|
|
22
|
+
"README.md",
|
|
23
|
+
"kryptheon-replay.js"
|
|
22
24
|
],
|
|
23
25
|
"engines": {
|
|
24
26
|
"node": ">=20.6.0"
|
|
@@ -34,5 +36,13 @@
|
|
|
34
36
|
"testing",
|
|
35
37
|
"end-to-end",
|
|
36
38
|
"cli"
|
|
37
|
-
]
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/jhashantanu987-beep/kryptheon-cli.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/jhashantanu987-beep/kryptheon-cli/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/jhashantanu987-beep/kryptheon-cli#readme"
|
|
38
48
|
}
|