kryptheon 0.1.18 → 0.1.20
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 +18 -10
- package/bin/kryptheon.js +119 -13
- package/kryptheon-fixture.js +7 -3
- package/kryptheon-project.js +21 -0
- package/kryptheon-reporter.js +18 -6
- package/kryptheon-secrets.js +5 -3
- package/kryptheon-store.js +317 -0
- package/package.json +80 -78
- package/playwright.config.js +5 -2
package/README.md
CHANGED
|
@@ -161,20 +161,28 @@ wanted, and one new name of the same kind. A button that is gone with nothing
|
|
|
161
161
|
in its place, or a rename next to other new buttons, fails as it always did.
|
|
162
162
|
Checks you added with the toolbar are never redirected.
|
|
163
163
|
|
|
164
|
-
##
|
|
164
|
+
## What goes where
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
Only your recordings go in your project, in `tests/`. Everything Kryptheon
|
|
167
|
+
remembers about the project is kept outside it, so it can never end up in your
|
|
168
|
+
repo:
|
|
169
|
+
|
|
170
|
+
~/.kryptheon/projects/<folder-name>-<id>/
|
|
171
|
+
baselines.json the remembered result for each test
|
|
172
|
+
history.jsonl one line per run, used for "this was working on …"
|
|
173
|
+
test-results/ the screenshot of the last failure
|
|
174
|
+
|
|
175
|
+
Set `KRYPTHEON_HOME` to keep them somewhere else. Each project folder gets its
|
|
176
|
+
own store, so two projects are never mixed. A project folder that is moved or
|
|
177
|
+
renamed starts a new store.
|
|
178
|
+
|
|
179
|
+
Older versions kept `kryptheon-baselines.json`, `kryptheon-history.jsonl` and
|
|
180
|
+
`test-results/` in the project. The first run of this version moves them out,
|
|
181
|
+
once, and says so.
|
|
171
182
|
|
|
172
183
|
A `.env` in the same folder is loaded automatically, and anything you type into a
|
|
173
184
|
password box is replaced with an environment variable rather than written into
|
|
174
|
-
the test.
|
|
175
|
-
|
|
176
|
-
Worth adding to `.gitignore`: `.env`, `kryptheon-baselines.json`,
|
|
177
|
-
`kryptheon-history.jsonl`, `test-results/`.
|
|
185
|
+
the test. When there is a password, `.env` is added to an existing `.gitignore`.
|
|
178
186
|
|
|
179
187
|
## Requirements
|
|
180
188
|
|
package/bin/kryptheon.js
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Two roots matter, and they are not the same once this is installed:
|
|
7
7
|
// PACKAGE_DIR - the tool's own files (fixture, reporter, config, this file)
|
|
8
|
-
// USER_DIR - the folder the command was run in, which owns tests
|
|
9
|
-
//
|
|
8
|
+
// USER_DIR - the folder the command was run in, which owns tests/ and .env
|
|
9
|
+
//
|
|
10
|
+
// Everything Kryptheon remembers about USER_DIR - baselines, history, the last
|
|
11
|
+
// run's screenshots - is kept in its store under ~/.kryptheon, never in the
|
|
12
|
+
// project (kryptheon-store.js).
|
|
10
13
|
|
|
11
14
|
const fs = require('fs');
|
|
12
15
|
const path = require('path');
|
|
@@ -19,6 +22,11 @@ const recordings = require('../kryptheon-recordings.js');
|
|
|
19
22
|
const cleanup = require('../kryptheon-cleanup.js');
|
|
20
23
|
const baselines = require('../kryptheon-baselines.js');
|
|
21
24
|
const project = require('../kryptheon-project.js');
|
|
25
|
+
const store = require('../kryptheon-store.js');
|
|
26
|
+
|
|
27
|
+
// The first version that keeps its records in the store. An older copy in a
|
|
28
|
+
// project folder still looks for them inside the project.
|
|
29
|
+
const FIRST_STORE_VERSION = '0.1.20';
|
|
22
30
|
|
|
23
31
|
const PACKAGE_DIR = path.join(__dirname, '..');
|
|
24
32
|
const USER_DIR = process.cwd();
|
|
@@ -864,15 +872,49 @@ function takeOutSecrets(relativeFile) {
|
|
|
864
872
|
return writeSpec(relativeFile, result.source) ? result.replacements : [];
|
|
865
873
|
}
|
|
866
874
|
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
|
|
875
|
+
// The password file is the one thing of kryptheon's in a project that must not
|
|
876
|
+
// be committed. If there is already a .gitignore, it belongs in it - but only
|
|
877
|
+
// once there is a password to protect: a .env here, or a recording that just
|
|
878
|
+
// had one taken out. A landing page with no sign-in has nothing to hide, and
|
|
879
|
+
// an edit to its .gitignore would be one more change of Kryptheon's in the repo.
|
|
880
|
+
function keepArtefactsOutOfGit(passwordTakenOut) {
|
|
881
|
+
if (!passwordTakenOut && !fs.existsSync(path.join(USER_DIR, '.env'))) return;
|
|
870
882
|
const added = secrets.updateGitignore(USER_DIR);
|
|
871
883
|
if (!added.length) return;
|
|
872
884
|
console.log(' Added to .gitignore: ' + added.join(', '));
|
|
873
885
|
console.log('');
|
|
874
886
|
}
|
|
875
887
|
|
|
888
|
+
// The project's store, opened before anything reads a record, so whatever an
|
|
889
|
+
// older version left in this folder is moved out first - and said once, here,
|
|
890
|
+
// rather than by the fixture or the reporter, which run inside the test runner.
|
|
891
|
+
//
|
|
892
|
+
// Returns null when the store cannot be made. Carrying on would mean every
|
|
893
|
+
// recording is compared against nothing, which reads as "all fine" and is
|
|
894
|
+
// the one answer that must never be given by accident.
|
|
895
|
+
function openStore() {
|
|
896
|
+
let opened;
|
|
897
|
+
try {
|
|
898
|
+
opened = store.open(USER_DIR);
|
|
899
|
+
} catch (err) {
|
|
900
|
+
console.error('');
|
|
901
|
+
console.error(' Kryptheon keeps what it remembers about this project in');
|
|
902
|
+
console.error(' ' + store.dirFor(USER_DIR));
|
|
903
|
+
console.error(' and could not write there: ' + err.message);
|
|
904
|
+
console.error('');
|
|
905
|
+
console.error(' Without it every recording would look new, so nothing was checked.');
|
|
906
|
+
console.error(' Set KRYPTHEON_HOME to a folder you can write to, and run this again.');
|
|
907
|
+
console.error('');
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
const lines = store.migrationLines(opened);
|
|
911
|
+
if (lines.length) {
|
|
912
|
+
console.log('');
|
|
913
|
+
for (const line of lines) console.log(line);
|
|
914
|
+
}
|
|
915
|
+
return opened;
|
|
916
|
+
}
|
|
917
|
+
|
|
876
918
|
function reportSecrets(replacements) {
|
|
877
919
|
if (!replacements || !replacements.length) return;
|
|
878
920
|
const names = replacements.map((item) => item.envName);
|
|
@@ -1170,7 +1212,7 @@ async function finaliseRecording(outFile, context) {
|
|
|
1170
1212
|
|
|
1171
1213
|
reportTidying(tidied);
|
|
1172
1214
|
reportSecrets(secrets);
|
|
1173
|
-
keepArtefactsOutOfGit();
|
|
1215
|
+
keepArtefactsOutOfGit(!!(secrets && secrets.length));
|
|
1174
1216
|
await offerToDropLogout(named);
|
|
1175
1217
|
reportReplayRisks(named);
|
|
1176
1218
|
reportFragileSelectors(named);
|
|
@@ -1321,6 +1363,7 @@ async function remove(name) {
|
|
|
1321
1363
|
console.error("");
|
|
1322
1364
|
return 1;
|
|
1323
1365
|
}
|
|
1366
|
+
if (!openStore()) return 1;
|
|
1324
1367
|
|
|
1325
1368
|
const gone = [];
|
|
1326
1369
|
const failed = [];
|
|
@@ -1365,15 +1408,20 @@ function writeOwnPackageJson(dir) {
|
|
|
1365
1408
|
}
|
|
1366
1409
|
}
|
|
1367
1410
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1411
|
+
// `npm install --save-dev <spec>` in a folder. npm is npm.cmd on Windows, and
|
|
1412
|
+
// Node will not start a .cmd directly (spawnSync EINVAL), so there it goes
|
|
1413
|
+
// through cmd.exe. The spec is always "kryptheon" or "kryptheon@" and a
|
|
1414
|
+
// version made of digits and dots; nothing anybody typed reaches this line.
|
|
1415
|
+
function npmInstallDev(dir, spec) {
|
|
1416
|
+
const args = ['install', '--save-dev', spec, '--no-audit', '--no-fund', '--loglevel=error'];
|
|
1417
|
+
return process.platform === 'win32'
|
|
1375
1418
|
? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'npm'].concat(args), { cwd: dir, encoding: 'utf8' })
|
|
1376
1419
|
: spawnSync('npm', args, { cwd: dir, encoding: 'utf8' });
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function installKryptheonIn(dir) {
|
|
1423
|
+
console.log(' Installing kryptheon here - this usually takes under a minute.');
|
|
1424
|
+
const result = npmInstallDev(dir, 'kryptheon');
|
|
1377
1425
|
// Judged by what is on disk afterwards, not by npm's exit code alone.
|
|
1378
1426
|
const ok = !result.error && result.status === 0 && project.kryptheonInstalledIn(dir);
|
|
1379
1427
|
if (ok) {
|
|
@@ -1489,6 +1537,9 @@ async function record(url) {
|
|
|
1489
1537
|
// gets a folder set up; before the browser, which is the expensive part.
|
|
1490
1538
|
const setup = await prepareFolder(url);
|
|
1491
1539
|
if (setup !== null) return setup;
|
|
1540
|
+
// Before an old recording can be replaced, because replacing one also drops
|
|
1541
|
+
// its saved result - which has to be found where it now lives.
|
|
1542
|
+
if (!openStore()) return 1;
|
|
1492
1543
|
|
|
1493
1544
|
if (!ensureBrowser()) return 1;
|
|
1494
1545
|
|
|
@@ -1700,6 +1751,7 @@ function check(options) {
|
|
|
1700
1751
|
for (const line of project.noProjectLines(here)) console.error(line);
|
|
1701
1752
|
return 1;
|
|
1702
1753
|
}
|
|
1754
|
+
if (!openStore()) return 1;
|
|
1703
1755
|
|
|
1704
1756
|
// Deliberately not a failure. An assistant told to run this after every
|
|
1705
1757
|
// change will read a non-zero exit as "something broke" and start fixing
|
|
@@ -1978,6 +2030,9 @@ function listBaselines(api) {
|
|
|
1978
2030
|
}
|
|
1979
2031
|
|
|
1980
2032
|
function accept(name) {
|
|
2033
|
+
// Only where there are recordings: anywhere else there is nothing to accept,
|
|
2034
|
+
// and no reason to make a store for a folder that is not a project.
|
|
2035
|
+
if (fs.existsSync(TESTS_DIR) && !openStore()) return 1;
|
|
1981
2036
|
const api = baselineApi();
|
|
1982
2037
|
if (!api) return 1;
|
|
1983
2038
|
if (!name) return listBaselines(api);
|
|
@@ -2085,6 +2140,57 @@ function handToFolderCopy() {
|
|
|
2085
2140
|
/* said as unknown */
|
|
2086
2141
|
}
|
|
2087
2142
|
const mine = packageVersion();
|
|
2143
|
+
|
|
2144
|
+
// An older copy is brought up to this one first. Found the day 0.1.18
|
|
2145
|
+
// shipped: the fix was on the registry, `npx kryptheon@latest check` and
|
|
2146
|
+
// every AI tool asked for it, and the folder's 0.1.17 ran anyway - the
|
|
2147
|
+
// person would have had to type an npm command to get what they had
|
|
2148
|
+
// already asked for. Never downwards: a folder that is ahead of a cached
|
|
2149
|
+
// npx copy keeps what it has.
|
|
2150
|
+
if (project.olderVersion(theirVersion, mine)) {
|
|
2151
|
+
const root = path.dirname(path.dirname(theirs)); // <root>/node_modules/kryptheon
|
|
2152
|
+
if (project.hasPackageJson(root)) {
|
|
2153
|
+
console.log('');
|
|
2154
|
+
console.log(' This folder has kryptheon ' + theirVersion + ' and this is ' + mine + '. Updating the folder\'s');
|
|
2155
|
+
console.log(' copy, because your recordings load that one - usually under a minute.');
|
|
2156
|
+
const result = npmInstallDev(root, 'kryptheon@' + mine);
|
|
2157
|
+
let now = null;
|
|
2158
|
+
try {
|
|
2159
|
+
now = JSON.parse(fs.readFileSync(path.join(theirs, 'package.json'), 'utf8')).version;
|
|
2160
|
+
} catch (err) {
|
|
2161
|
+
now = null;
|
|
2162
|
+
}
|
|
2163
|
+
// Believed only when the folder now holds exactly this version.
|
|
2164
|
+
if (!result.error && result.status === 0 && now === mine && project.kryptheonCopyFor(USER_DIR) === theirs) {
|
|
2165
|
+
console.log(' Updated.');
|
|
2166
|
+
theirVersion = now;
|
|
2167
|
+
} else {
|
|
2168
|
+
console.log(' Could not update it just now (no internet?), so the ' + (now || theirVersion) + ' that is here runs.');
|
|
2169
|
+
if (now) theirVersion = now;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// A copy from before the store looks for its records inside the project. If
|
|
2175
|
+
// they have already moved out, it finds none, takes every recording as new,
|
|
2176
|
+
// and learns whatever the app does today as correct - a broken page would
|
|
2177
|
+
// pass. So it is not run; the one fix is to bring it up to date.
|
|
2178
|
+
if (project.olderVersion(theirVersion, FIRST_STORE_VERSION)) {
|
|
2179
|
+
const kept = store.pathsFor(USER_DIR);
|
|
2180
|
+
if (fs.existsSync(kept.baselines) || fs.existsSync(kept.history)) {
|
|
2181
|
+
console.error('');
|
|
2182
|
+
console.error(' This folder has kryptheon ' + theirVersion + ', which looks for its saved results');
|
|
2183
|
+
console.error(' inside the project. They have moved out, to');
|
|
2184
|
+
console.error(' ' + kept.dir);
|
|
2185
|
+
console.error(' so ' + theirVersion + ' would see none, treat every recording as new, and could');
|
|
2186
|
+
console.error(' miss a real change. Nothing was checked. Update this folder\'s copy:');
|
|
2187
|
+
console.error(' npm i -D kryptheon@latest');
|
|
2188
|
+
console.error(' then run this again.');
|
|
2189
|
+
console.error('');
|
|
2190
|
+
return 1;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2088
2194
|
if (theirVersion !== mine) {
|
|
2089
2195
|
// Not an error: the folder's copy is the one its recordings are written
|
|
2090
2196
|
// against. Said, so an older one is never mistaken for this one.
|
package/kryptheon-fixture.js
CHANGED
|
@@ -26,10 +26,13 @@ const heal = require('./kryptheon-heal.js');
|
|
|
26
26
|
|
|
27
27
|
const MAX_ITEMS = 5; // keep the failure block readable
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const store = require('./kryptheon-store.js');
|
|
30
|
+
|
|
31
|
+
// Baselines describe the user's app, so they belong to the folder the command
|
|
32
|
+
// was run from - but they are kept in that folder's store, outside it, so the
|
|
33
|
+
// project's repo never grows because Kryptheon is watching it.
|
|
31
34
|
const USER_DIR = process.cwd();
|
|
32
|
-
const BASELINE_FILE =
|
|
35
|
+
const BASELINE_FILE = store.pathsFor(USER_DIR).baselines;
|
|
33
36
|
|
|
34
37
|
function oneLine(text) {
|
|
35
38
|
return String(text).replace(/\s+/g, ' ').trim();
|
|
@@ -78,6 +81,7 @@ function saveBaselines(file, all) {
|
|
|
78
81
|
try {
|
|
79
82
|
// Explicit, and without a byte order mark: this file is read back by the
|
|
80
83
|
// same code and by people, and a mismatch would look like the app changed.
|
|
84
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
81
85
|
fs.writeFileSync(file, JSON.stringify(all, null, 2) + '\n', { encoding: 'utf8' });
|
|
82
86
|
return true;
|
|
83
87
|
} catch (e) {
|
package/kryptheon-project.js
CHANGED
|
@@ -526,8 +526,29 @@ function installFailedLines() {
|
|
|
526
526
|
];
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Whether version `a` is older than `b`, by their numbers. Anything that is
|
|
531
|
+
* not plain x.y.z is never called older: updating on a guess would change
|
|
532
|
+
* somebody's package.json for nothing, and only digits and dots ever reach
|
|
533
|
+
* the npm command line built from the answer.
|
|
534
|
+
*/
|
|
535
|
+
function olderVersion(a, b) {
|
|
536
|
+
const parse = (v) => {
|
|
537
|
+
const m = String(v == null ? '' : v).match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
538
|
+
return m ? m.slice(1).map(Number) : null;
|
|
539
|
+
};
|
|
540
|
+
const x = parse(a);
|
|
541
|
+
const y = parse(b);
|
|
542
|
+
if (!x || !y) return false;
|
|
543
|
+
for (let i = 0; i < 3; i++) {
|
|
544
|
+
if (x[i] !== y[i]) return x[i] < y[i];
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
|
|
529
549
|
module.exports = {
|
|
530
550
|
ESCAPE_HATCH: ESCAPE_HATCH,
|
|
551
|
+
olderVersion: olderVersion,
|
|
531
552
|
HOME_RECORDINGS: HOME_RECORDINGS,
|
|
532
553
|
kryptheonReachableFrom: kryptheonReachableFrom,
|
|
533
554
|
kryptheonCopyFor: kryptheonCopyFor,
|
package/kryptheon-reporter.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
// Plain-language Playwright reporter for non-technical readers.
|
|
2
2
|
// Prints a short line per passing test and a readable block per failure,
|
|
3
|
-
// and appends one JSON line per run to
|
|
3
|
+
// and appends one JSON line per run to the project's history.jsonl.
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
6
7
|
const path = require('path');
|
|
7
8
|
const recordings = require('./kryptheon-recordings.js');
|
|
8
9
|
const heal = require('./kryptheon-heal.js');
|
|
10
|
+
const store = require('./kryptheon-store.js');
|
|
9
11
|
|
|
10
12
|
// An assistant runs this after every change, so it needs a mode that says
|
|
11
13
|
// nothing at all when the news is good.
|
|
12
14
|
const QUIET = !!process.env.KRYPTHEON_QUIET;
|
|
13
15
|
|
|
14
|
-
// The history belongs to whoever is running the tests, so it
|
|
15
|
-
//
|
|
16
|
+
// The history belongs to whoever is running the tests, so it is kept in their
|
|
17
|
+
// project's store - not inside the installed package, and not in the project.
|
|
16
18
|
const USER_DIR = process.cwd();
|
|
17
|
-
const HISTORY_FILE =
|
|
19
|
+
const HISTORY_FILE = store.pathsFor(USER_DIR).history;
|
|
20
|
+
|
|
21
|
+
// Said with ~ for the home folder: the full path is long, and the person only
|
|
22
|
+
// needs to know it is outside their project and where to look.
|
|
23
|
+
function shortHome(file) {
|
|
24
|
+
const home = os.homedir();
|
|
25
|
+
return file.toLowerCase().startsWith(home.toLowerCase() + path.sep) ? '~' + file.slice(home.length) : file;
|
|
26
|
+
}
|
|
18
27
|
|
|
19
28
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'];
|
|
20
29
|
|
|
@@ -962,10 +971,13 @@ class KryptheonReporter {
|
|
|
962
971
|
};
|
|
963
972
|
|
|
964
973
|
// Append only - existing lines are never rewritten.
|
|
974
|
+
let saved = false;
|
|
965
975
|
try {
|
|
976
|
+
fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
966
977
|
fs.appendFileSync(HISTORY_FILE, JSON.stringify(entry) + '\n', 'utf8');
|
|
978
|
+
saved = true;
|
|
967
979
|
} catch (err) {
|
|
968
|
-
process.stdout.write('\n(could not write ' +
|
|
980
|
+
process.stdout.write('\n(could not write ' + shortHome(HISTORY_FILE) + ': ' + err.message + ')\n');
|
|
969
981
|
}
|
|
970
982
|
|
|
971
983
|
// "Broken" is kept for recordings that used to work. The ones that never
|
|
@@ -1011,7 +1023,7 @@ class KryptheonReporter {
|
|
|
1011
1023
|
|
|
1012
1024
|
process.stdout.write(
|
|
1013
1025
|
'\n' + recordings.summaryLines(counts).join('\n') + '\n' +
|
|
1014
|
-
'History saved
|
|
1026
|
+
(saved ? 'History saved outside your project, in ' + shortHome(HISTORY_FILE) + '\n' : '') + '\n'
|
|
1015
1027
|
);
|
|
1016
1028
|
|
|
1017
1029
|
// Once per run, not once per test: it is one fact about the app, and
|
package/kryptheon-secrets.js
CHANGED
|
@@ -139,9 +139,11 @@ function writeEnvExample(dir, names) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
142
|
+
// What must never be committed from a project: the real password. The three
|
|
143
|
+
// files of machine state that used to be listed here - baselines, history and
|
|
144
|
+
// test-results/ - are no longer written into the project at all; they live in
|
|
145
|
+
// its store (kryptheon-store.js), so there is nothing of Kryptheon's to ignore.
|
|
146
|
+
const GITIGNORE_ENTRIES = ['.env'];
|
|
145
147
|
|
|
146
148
|
/**
|
|
147
149
|
* Adds the missing entries to an existing .gitignore. Returns what it added.
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Where Kryptheon keeps what it remembers about a project.
|
|
2
|
+
//
|
|
3
|
+
// It used to be the project itself: kryptheon-baselines.json,
|
|
4
|
+
// kryptheon-history.jsonl, test-results/ and .kryptheon-last.json all appeared
|
|
5
|
+
// next to the person's own code. A plain landing page picked up six or seven
|
|
6
|
+
// files that way, and an AI assistant that commits "everything that changed"
|
|
7
|
+
// commits them too - the repo grows because Kryptheon is watching it.
|
|
8
|
+
//
|
|
9
|
+
// So every record now lives in one folder per project, outside it:
|
|
10
|
+
//
|
|
11
|
+
// ~/.kryptheon/projects/<folder-name>-<hash>/ (KRYPTHEON_HOME moves ~/.kryptheon)
|
|
12
|
+
// project.json which folder this store belongs to
|
|
13
|
+
// baselines.json was kryptheon-baselines.json
|
|
14
|
+
// history.jsonl was kryptheon-history.jsonl
|
|
15
|
+
// test-results/ was test-results/ (Playwright's screenshots of a failure)
|
|
16
|
+
// night-last.json was .kryptheon-last.json (kryptheon-night)
|
|
17
|
+
//
|
|
18
|
+
// Only tests/ - the recordings the person made - stays in the project.
|
|
19
|
+
//
|
|
20
|
+
// The hash is of the project's full path, so two folders that happen to share
|
|
21
|
+
// a name are never mixed. kryptheon-night carries a copy of this file and must
|
|
22
|
+
// land in the same folder for the same project; both packages check the ids
|
|
23
|
+
// below against the same fixed examples, so the two copies cannot drift apart.
|
|
24
|
+
//
|
|
25
|
+
// Records an older version left in a project are moved here the first time a
|
|
26
|
+
// project is opened, and only moved: nothing is thrown away, and when both
|
|
27
|
+
// places already hold a file the project's copy is kept in parked/ rather than
|
|
28
|
+
// overwriting what is here.
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const crypto = require('crypto');
|
|
34
|
+
|
|
35
|
+
const HOME_ENV = 'KRYPTHEON_HOME';
|
|
36
|
+
|
|
37
|
+
const FILES = {
|
|
38
|
+
project: 'project.json',
|
|
39
|
+
baselines: 'baselines.json',
|
|
40
|
+
history: 'history.jsonl',
|
|
41
|
+
testResults: 'test-results',
|
|
42
|
+
nightLast: 'night-last.json',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// What older versions wrote into the project, and where each one goes now.
|
|
46
|
+
// append - history: lines the store does not have yet are added to it
|
|
47
|
+
// park - both exist: the project's copy goes to parked/, the store's stays
|
|
48
|
+
const LEGACY = [
|
|
49
|
+
{ from: 'kryptheon-baselines.json', to: FILES.baselines, onBoth: 'park' },
|
|
50
|
+
{ from: 'kryptheon-history.jsonl', to: FILES.history, onBoth: 'append' },
|
|
51
|
+
{ from: '.kryptheon-last.json', to: FILES.nightLast, onBoth: 'park' },
|
|
52
|
+
{ from: 'test-results', to: FILES.testResults, onBoth: 'park', dir: true },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// A project with a Playwright config of its own owns its test-results/ -
|
|
56
|
+
// Playwright's default output folder. Moving that would take the person's own
|
|
57
|
+
// screenshots away, so it is left exactly where it is.
|
|
58
|
+
const OWN_PLAYWRIGHT = /^playwright\.config\.(js|cjs|mjs|ts|cts|mts)$/;
|
|
59
|
+
|
|
60
|
+
/** ~/.kryptheon, or wherever KRYPTHEON_HOME points. */
|
|
61
|
+
function home(env) {
|
|
62
|
+
const source = env || process.env;
|
|
63
|
+
const set = source[HOME_ENV];
|
|
64
|
+
if (set && String(set).trim()) return path.resolve(String(set).trim());
|
|
65
|
+
return path.join(os.homedir(), '.kryptheon');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The project folder as one exact path, whatever form it was given in. */
|
|
69
|
+
function canonicalRoot(dir) {
|
|
70
|
+
const resolved = path.resolve(dir || process.cwd());
|
|
71
|
+
try {
|
|
72
|
+
return fs.realpathSync.native(resolved);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* <folder-name>-<12 hex>. Pure: the same path always gives the same id.
|
|
80
|
+
*
|
|
81
|
+
* Windows paths are compared without regard to case, because C:\Users\a and
|
|
82
|
+
* c:\users\a are the same folder there and must be the same project.
|
|
83
|
+
*/
|
|
84
|
+
function projectId(root, platform) {
|
|
85
|
+
const plat = platform || process.platform;
|
|
86
|
+
const P = plat === 'win32' ? path.win32 : path.posix;
|
|
87
|
+
// resolve() also drops a trailing separator and turns / into \ on Windows.
|
|
88
|
+
let key = P.resolve(String(root));
|
|
89
|
+
if (plat === 'win32') key = key.toLowerCase();
|
|
90
|
+
const hash = crypto.createHash('sha256').update(key, 'utf8').digest('hex').slice(0, 12);
|
|
91
|
+
const base = P.basename(key) || 'project';
|
|
92
|
+
const name = base.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|-+$/g, '').slice(0, 40) || 'project';
|
|
93
|
+
return name + '-' + hash;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The store folder for a project. Only reads: nothing is created or moved, so
|
|
98
|
+
* the fixture and the reporter can ask for it just by being loaded.
|
|
99
|
+
*/
|
|
100
|
+
function dirFor(dir, env) {
|
|
101
|
+
const root = canonicalRoot(dir);
|
|
102
|
+
const short = path.join(home(env), 'projects', projectId(root));
|
|
103
|
+
const owner = readProject(path.join(short, FILES.project));
|
|
104
|
+
if (!owner || !owner.root || sameRoot(owner.root, root)) return short;
|
|
105
|
+
// Two different folders with the same short hash. Astronomically rare, but
|
|
106
|
+
// the one thing a store must never do is hold two projects, so the second
|
|
107
|
+
// one gets the full hash instead.
|
|
108
|
+
const full = crypto.createHash('sha256').update(root, 'utf8').digest('hex');
|
|
109
|
+
return short.replace(/-[0-9a-f]{12}$/, '-' + full);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Every file in a project's store, by name. Only reads, like dirFor. */
|
|
113
|
+
function pathsFor(dir, env) {
|
|
114
|
+
return pathsIn(dirFor(dir, env));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pathsIn(dir) {
|
|
118
|
+
return {
|
|
119
|
+
dir: dir,
|
|
120
|
+
project: path.join(dir, FILES.project),
|
|
121
|
+
baselines: path.join(dir, FILES.baselines),
|
|
122
|
+
history: path.join(dir, FILES.history),
|
|
123
|
+
testResults: path.join(dir, FILES.testResults),
|
|
124
|
+
nightLast: path.join(dir, FILES.nightLast),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function exists(file) {
|
|
129
|
+
try {
|
|
130
|
+
fs.lstatSync(file);
|
|
131
|
+
return true;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasOwnPlaywright(root) {
|
|
138
|
+
try {
|
|
139
|
+
return fs.readdirSync(root).some((name) => OWN_PLAYWRIGHT.test(name));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// rename is the whole move when both sides are on one drive. Across drives it
|
|
146
|
+
// fails with EXDEV, and then it is copy, check the copy, and only then remove.
|
|
147
|
+
function move(from, to, isDir) {
|
|
148
|
+
try {
|
|
149
|
+
fs.renameSync(from, to);
|
|
150
|
+
return;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
// Anything else - a file held open by an editor, say - is reported and the
|
|
153
|
+
// record stays where it is, whole.
|
|
154
|
+
if (err.code !== 'EXDEV') throw err;
|
|
155
|
+
}
|
|
156
|
+
if (isDir) {
|
|
157
|
+
fs.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
158
|
+
fs.rmSync(from, { recursive: true, force: true });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const tmp = to + '.moving';
|
|
162
|
+
fs.copyFileSync(from, tmp);
|
|
163
|
+
if (fs.statSync(tmp).size !== fs.statSync(from).size) {
|
|
164
|
+
fs.rmSync(tmp, { force: true });
|
|
165
|
+
throw new Error('the copy of ' + from + ' came out a different size');
|
|
166
|
+
}
|
|
167
|
+
fs.renameSync(tmp, to);
|
|
168
|
+
fs.rmSync(from, { force: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Lines of the project's history the store does not have yet, in their order.
|
|
172
|
+
// Checked line by line so moving twice - say the project file could not be
|
|
173
|
+
// removed the first time - never writes a run down twice.
|
|
174
|
+
function appendHistory(from, to) {
|
|
175
|
+
const have = new Set(fs.readFileSync(to, 'utf8').split('\n').filter((l) => l.trim()));
|
|
176
|
+
const extra = fs.readFileSync(from, 'utf8').split('\n').filter((l) => l.trim() && !have.has(l));
|
|
177
|
+
if (extra.length) {
|
|
178
|
+
const current = fs.readFileSync(to, 'utf8');
|
|
179
|
+
const lead = current.length && !current.endsWith('\n') ? '\n' : '';
|
|
180
|
+
fs.appendFileSync(to, lead + extra.join('\n') + '\n', 'utf8');
|
|
181
|
+
}
|
|
182
|
+
fs.rmSync(from, { force: true });
|
|
183
|
+
return extra.length;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stamp(now) {
|
|
187
|
+
return (now || new Date()).toISOString().replace(/[:.]/g, '-');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Moves what an older version left in the project into the store.
|
|
192
|
+
* Returns { moved: [names], parked: [{ name, to }], appended: [{ name, lines }],
|
|
193
|
+
* kept: [{ name, why }], failed: [{ name, why }] }.
|
|
194
|
+
*/
|
|
195
|
+
function migrate(root, dir, options) {
|
|
196
|
+
const out = { moved: [], parked: [], appended: [], kept: [], failed: [] };
|
|
197
|
+
const ownPlaywright = hasOwnPlaywright(root);
|
|
198
|
+
for (const item of LEGACY) {
|
|
199
|
+
const from = path.join(root, item.from);
|
|
200
|
+
if (!exists(from)) continue;
|
|
201
|
+
const label = item.from + (item.dir ? '/' : '');
|
|
202
|
+
if (item.dir && ownPlaywright) {
|
|
203
|
+
out.kept.push({ name: label, why: 'this project has its own Playwright config, so it is theirs' });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const to = path.join(dir, item.to);
|
|
207
|
+
try {
|
|
208
|
+
if (!exists(to)) {
|
|
209
|
+
move(from, to, item.dir);
|
|
210
|
+
out.moved.push(label);
|
|
211
|
+
} else if (item.onBoth === 'append') {
|
|
212
|
+
out.appended.push({ name: label, lines: appendHistory(from, to) });
|
|
213
|
+
} else {
|
|
214
|
+
const parkedDir = path.join(dir, 'parked');
|
|
215
|
+
fs.mkdirSync(parkedDir, { recursive: true });
|
|
216
|
+
const parked = path.join(parkedDir, stamp(options && options.now) + '-' + item.from.replace(/^\./, ''));
|
|
217
|
+
move(from, parked, item.dir);
|
|
218
|
+
out.parked.push({ name: label, to: parked });
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
out.failed.push({ name: label, why: err.message });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The store for the project in `dir` (default: the current folder), created if
|
|
229
|
+
* it is not there yet, with anything an older version left in the project moved
|
|
230
|
+
* into it. Safe to call again: once moved, there is nothing left to move.
|
|
231
|
+
*
|
|
232
|
+
* Throws when the store cannot be created. A record that silently cannot be
|
|
233
|
+
* written makes every run look like the first one - which reads as "nothing to
|
|
234
|
+
* compare against, all fine" - so that is never swallowed here.
|
|
235
|
+
*/
|
|
236
|
+
function open(dir, options) {
|
|
237
|
+
const env = (options && options.env) || process.env;
|
|
238
|
+
const root = canonicalRoot(dir);
|
|
239
|
+
const storeDir = dirFor(root, env);
|
|
240
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
241
|
+
const paths = pathsIn(storeDir);
|
|
242
|
+
if (!exists(paths.project)) {
|
|
243
|
+
fs.writeFileSync(paths.project, JSON.stringify({
|
|
244
|
+
root: root,
|
|
245
|
+
name: path.basename(root),
|
|
246
|
+
id: path.basename(storeDir),
|
|
247
|
+
createdAt: new Date().toISOString(),
|
|
248
|
+
}, null, 2) + '\n', 'utf8');
|
|
249
|
+
}
|
|
250
|
+
return Object.assign({ root: root, id: path.basename(storeDir) }, paths, {
|
|
251
|
+
migration: migrate(root, storeDir, options),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sameRoot(a, b, platform) {
|
|
256
|
+
const plat = platform || process.platform;
|
|
257
|
+
const norm = (p) => {
|
|
258
|
+
const r = path.resolve(String(p)).replace(/[\\/]+$/, '');
|
|
259
|
+
return plat === 'win32' ? r.split('/').join('\\').toLowerCase() : r;
|
|
260
|
+
};
|
|
261
|
+
return norm(a) === norm(b);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readProject(file) {
|
|
265
|
+
try {
|
|
266
|
+
let raw = fs.readFileSync(file, 'utf8');
|
|
267
|
+
if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
|
|
268
|
+
const parsed = JSON.parse(raw);
|
|
269
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** What to tell the person about a migration, or nothing when nothing moved. */
|
|
276
|
+
function migrationLines(store) {
|
|
277
|
+
const m = store && store.migration;
|
|
278
|
+
if (!m) return [];
|
|
279
|
+
const lines = [];
|
|
280
|
+
const movedNames = m.moved.concat(m.appended.map((a) => a.name)).concat(m.parked.map((p) => p.name));
|
|
281
|
+
if (movedNames.length) {
|
|
282
|
+
lines.push(' Kryptheon now keeps its records outside your project, so they can never');
|
|
283
|
+
lines.push(' end up in your repo. Moved out of this folder:');
|
|
284
|
+
for (const name of movedNames) lines.push(' ' + name);
|
|
285
|
+
lines.push(' They are now in:');
|
|
286
|
+
lines.push(' ' + store.dir);
|
|
287
|
+
for (const p of m.parked) {
|
|
288
|
+
lines.push(' ' + p.name + ' was already there too, so this folder\'s copy is kept aside in:');
|
|
289
|
+
lines.push(' ' + p.to);
|
|
290
|
+
}
|
|
291
|
+
lines.push('');
|
|
292
|
+
}
|
|
293
|
+
for (const k of m.kept) {
|
|
294
|
+
lines.push(' Left ' + k.name + ' where it is: ' + k.why + '.');
|
|
295
|
+
lines.push('');
|
|
296
|
+
}
|
|
297
|
+
for (const f of m.failed) {
|
|
298
|
+
lines.push(' Could not move ' + f.name + ' out of this folder: ' + f.why);
|
|
299
|
+
lines.push(' It is still here. Close anything that has it open and run this again.');
|
|
300
|
+
lines.push('');
|
|
301
|
+
}
|
|
302
|
+
return lines;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
HOME_ENV: HOME_ENV,
|
|
307
|
+
FILES: FILES,
|
|
308
|
+
LEGACY: LEGACY,
|
|
309
|
+
home: home,
|
|
310
|
+
canonicalRoot: canonicalRoot,
|
|
311
|
+
projectId: projectId,
|
|
312
|
+
dirFor: dirFor,
|
|
313
|
+
pathsFor: pathsFor,
|
|
314
|
+
open: open,
|
|
315
|
+
migrate: migrate,
|
|
316
|
+
migrationLines: migrationLines,
|
|
317
|
+
};
|
package/package.json
CHANGED
|
@@ -1,78 +1,80 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "kryptheon",
|
|
3
|
-
"version": "0.1.
|
|
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-heal.js",
|
|
29
|
-
"kryptheon-selectors.js",
|
|
30
|
-
"kryptheon-recordings.js",
|
|
31
|
-
"kryptheon-cleanup.js",
|
|
32
|
-
"kryptheon-baselines.js",
|
|
33
|
-
"kryptheon-project.js"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"check
|
|
44
|
-
"check:
|
|
45
|
-
"check:
|
|
46
|
-
"check:
|
|
47
|
-
"check:
|
|
48
|
-
"check:
|
|
49
|
-
"check:
|
|
50
|
-
"check:
|
|
51
|
-
"check:
|
|
52
|
-
"check:
|
|
53
|
-
"check:
|
|
54
|
-
"check:
|
|
55
|
-
"check:
|
|
56
|
-
"check:
|
|
57
|
-
"check:
|
|
58
|
-
"check:
|
|
59
|
-
"check:
|
|
60
|
-
"check:
|
|
61
|
-
"check:
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "kryptheon",
|
|
3
|
+
"version": "0.1.20",
|
|
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-heal.js",
|
|
29
|
+
"kryptheon-selectors.js",
|
|
30
|
+
"kryptheon-recordings.js",
|
|
31
|
+
"kryptheon-cleanup.js",
|
|
32
|
+
"kryptheon-baselines.js",
|
|
33
|
+
"kryptheon-project.js",
|
|
34
|
+
"kryptheon-store.js"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20.6.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@playwright/test": "^1.62.1"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"check": "node kryptheon-store.check.js && 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-env.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-setup.check.js && node kryptheon-browser.check.js && node kryptheon-heal.check.js && node kryptheon-load.check.js && node kryptheon-package.check.js",
|
|
44
|
+
"check:store": "node kryptheon-store.check.js",
|
|
45
|
+
"check:reporter": "node kryptheon-reporter.check.js",
|
|
46
|
+
"check:signature": "node kryptheon-signature.check.js",
|
|
47
|
+
"check:stream": "node kryptheon-signature.stream.check.js",
|
|
48
|
+
"check:baseline": "node kryptheon-signature.baseline.check.js",
|
|
49
|
+
"check:settle": "node kryptheon-signature.settle.check.js",
|
|
50
|
+
"check:number": "node kryptheon-signature.number.check.js",
|
|
51
|
+
"check:env": "node kryptheon-env.check.js",
|
|
52
|
+
"check:watchdog": "node kryptheon-watchdog.check.js",
|
|
53
|
+
"check:record": "node kryptheon-record.check.js",
|
|
54
|
+
"check:selectors": "node kryptheon-selectors.check.js",
|
|
55
|
+
"check:recordings": "node kryptheon-recordings.check.js",
|
|
56
|
+
"check:cleanup": "node kryptheon-cleanup.check.js",
|
|
57
|
+
"check:orphan": "node kryptheon-orphan.check.js",
|
|
58
|
+
"check:project": "node kryptheon-project.check.js",
|
|
59
|
+
"check:setup": "node kryptheon-setup.check.js",
|
|
60
|
+
"check:browser": "node kryptheon-browser.check.js",
|
|
61
|
+
"check:heal": "node kryptheon-heal.check.js",
|
|
62
|
+
"check:load": "node kryptheon-load.check.js",
|
|
63
|
+
"check:package": "node kryptheon-package.check.js",
|
|
64
|
+
"prepublishOnly": "node kryptheon-package.check.js"
|
|
65
|
+
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"playwright",
|
|
68
|
+
"testing",
|
|
69
|
+
"end-to-end",
|
|
70
|
+
"cli"
|
|
71
|
+
],
|
|
72
|
+
"repository": {
|
|
73
|
+
"type": "git",
|
|
74
|
+
"url": "git+https://github.com/jhashantanu987-beep/kryptheon-cli.git"
|
|
75
|
+
},
|
|
76
|
+
"bugs": {
|
|
77
|
+
"url": "https://github.com/jhashantanu987-beep/kryptheon-cli/issues"
|
|
78
|
+
},
|
|
79
|
+
"homepage": "https://github.com/jhashantanu987-beep/kryptheon-cli#readme"
|
|
80
|
+
}
|
package/playwright.config.js
CHANGED
|
@@ -42,8 +42,11 @@ module.exports = defineConfig({
|
|
|
42
42
|
// The tests belong to the user, so they are found in their folder.
|
|
43
43
|
testDir: path.join(USER_DIR, 'tests'),
|
|
44
44
|
// Screenshots and other artefacts default to sitting next to the config,
|
|
45
|
-
// which once installed means inside node_modules.
|
|
46
|
-
|
|
45
|
+
// which once installed means inside node_modules. They belong to the user's
|
|
46
|
+
// project, so they go to its store - outside the project, where a commit of
|
|
47
|
+
// "everything that changed" cannot pick them up. Playwright empties this
|
|
48
|
+
// folder at the start of every run, so it never grows.
|
|
49
|
+
outputDir: require(path.join(PACKAGE_DIR, 'kryptheon-store.js')).pathsFor(USER_DIR).testResults,
|
|
47
50
|
// These tests all run against one live site, so in parallel they interfere
|
|
48
51
|
// with each other - a shared logged-in session, and rate limiting on the
|
|
49
52
|
// real endpoints. Serial is the safe default here.
|