kryptheon 0.1.11 → 0.1.13
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 +21 -0
- package/bin/kryptheon.js +128 -14
- package/kryptheon-fixture.js +431 -431
- package/kryptheon-project.js +324 -0
- package/kryptheon-reporter.js +130 -4
- package/kryptheon-secrets.js +191 -191
- package/kryptheon-signature.js +11 -11
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -6,6 +6,27 @@ plain English what broke.
|
|
|
6
6
|
Runs entirely on your machine. No account, no API key, nothing leaves your
|
|
7
7
|
computer. It is a wrapper around [Playwright](https://playwright.dev).
|
|
8
8
|
|
|
9
|
+
## Only have a web address?
|
|
10
|
+
|
|
11
|
+
Built your app in Lovable, Bolt or v0, and there is no project folder on your
|
|
12
|
+
computer? Open a terminal and run:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
npx kryptheon record https://your-app.lovable.app
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Before it opens anything, it looks at the folder you are in. If that folder is
|
|
19
|
+
not set up for recordings it says so and asks first, then adds a `package.json`
|
|
20
|
+
and kryptheon itself (about 20MB) — nothing else. A new terminal opens in your
|
|
21
|
+
home folder, so from there it keeps your recordings in a `kryptheon-tests`
|
|
22
|
+
folder of their own and tells you where that is.
|
|
23
|
+
|
|
24
|
+
After every change to your app, from that same folder:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
npx kryptheon check
|
|
28
|
+
```
|
|
29
|
+
|
|
9
30
|
## Install
|
|
10
31
|
|
|
11
32
|
```
|
package/bin/kryptheon.js
CHANGED
|
@@ -1116,7 +1116,9 @@ async function finaliseRecording(outFile, context) {
|
|
|
1116
1116
|
const named = await nameRecording(outFile);
|
|
1117
1117
|
console.log('');
|
|
1118
1118
|
console.log(' Saved to ' + named);
|
|
1119
|
-
|
|
1119
|
+
// npx, not the bare name: somebody who started with `npx kryptheon record`
|
|
1120
|
+
// has no `kryptheon` command, and would be told it does not exist.
|
|
1121
|
+
console.log(' Run it any time with: npx kryptheon check');
|
|
1120
1122
|
console.log('');
|
|
1121
1123
|
|
|
1122
1124
|
reportTidying(tidied);
|
|
@@ -1300,6 +1302,122 @@ async function remove(name) {
|
|
|
1300
1302
|
return failed.length ? 1 : 0;
|
|
1301
1303
|
}
|
|
1302
1304
|
|
|
1305
|
+
// --- setting the folder up --------------------------------------------------
|
|
1306
|
+
//
|
|
1307
|
+
// See the setup section of kryptheon-project.js for why this exists. That file
|
|
1308
|
+
// decides; this one asks, writes and installs.
|
|
1309
|
+
|
|
1310
|
+
function writeOwnPackageJson(dir) {
|
|
1311
|
+
// 'wx': never over the top of one that appeared since the folder was looked
|
|
1312
|
+
// at. Somebody's package.json is not ours to replace.
|
|
1313
|
+
try {
|
|
1314
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ private: true }, null, 2) + '\n', { flag: 'wx' });
|
|
1315
|
+
return true;
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
return err.code === 'EEXIST';
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function installKryptheonIn(dir) {
|
|
1322
|
+
console.log(' Installing kryptheon here - this usually takes under a minute.');
|
|
1323
|
+
const args = ['install', '--save-dev', 'kryptheon', '--no-audit', '--no-fund', '--loglevel=error'];
|
|
1324
|
+
// npm is npm.cmd on Windows, and Node will not start a .cmd directly
|
|
1325
|
+
// (spawnSync EINVAL), so there it goes through cmd.exe. Every argument is a
|
|
1326
|
+
// fixed string; nothing anybody typed reaches this command line.
|
|
1327
|
+
const result = process.platform === 'win32'
|
|
1328
|
+
? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'npm'].concat(args), { cwd: dir, encoding: 'utf8' })
|
|
1329
|
+
: spawnSync('npm', args, { cwd: dir, encoding: 'utf8' });
|
|
1330
|
+
// Judged by what is on disk afterwards, not by npm's exit code alone.
|
|
1331
|
+
const ok = !result.error && result.status === 0 && project.kryptheonInstalledIn(dir);
|
|
1332
|
+
if (ok) {
|
|
1333
|
+
console.log(' Installed.');
|
|
1334
|
+
console.log('');
|
|
1335
|
+
}
|
|
1336
|
+
return ok;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// A folder `record` is about to use, made ready: its own package.json, its own
|
|
1340
|
+
// copy of kryptheon. Returns false, having said why, if either did not happen.
|
|
1341
|
+
function makeReady(dir) {
|
|
1342
|
+
if (!project.hasPackageJson(dir) && !writeOwnPackageJson(dir)) {
|
|
1343
|
+
console.error('');
|
|
1344
|
+
console.error(' Could not create package.json in: ' + dir);
|
|
1345
|
+
console.error(' This folder may be read-only. Make a new folder of your own and');
|
|
1346
|
+
console.error(' run the same command from there.');
|
|
1347
|
+
console.error('');
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
if (!project.kryptheonInstalledIn(dir) && !installKryptheonIn(dir)) {
|
|
1351
|
+
for (const line of project.installFailedLines()) console.error(line);
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Run again from inside the new folder rather than moving this process there:
|
|
1358
|
+
// every path in this file is worked out once, from the folder the command
|
|
1359
|
+
// started in.
|
|
1360
|
+
async function recordFromHome(plan, url) {
|
|
1361
|
+
try {
|
|
1362
|
+
fs.mkdirSync(plan.into, { recursive: true });
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
console.error('');
|
|
1365
|
+
console.error(' Could not make the folder: ' + plan.into);
|
|
1366
|
+
console.error(' Make a new folder of your own, open a terminal in it, and run the');
|
|
1367
|
+
console.error(' same command again.');
|
|
1368
|
+
console.error('');
|
|
1369
|
+
return 1;
|
|
1370
|
+
}
|
|
1371
|
+
if (!makeReady(plan.into)) return 1;
|
|
1372
|
+
|
|
1373
|
+
const child = spawn(process.execPath, [__filename, 'record', url], { cwd: plan.into, stdio: 'inherit' });
|
|
1374
|
+
// Ctrl-C reaches the child too, and the child is the one that saves what was
|
|
1375
|
+
// recorded. This process waits for it rather than exiting under it.
|
|
1376
|
+
const wait = () => {};
|
|
1377
|
+
process.on('SIGINT', wait);
|
|
1378
|
+
process.on('SIGTERM', wait);
|
|
1379
|
+
const code = await new Promise((resolve) => {
|
|
1380
|
+
child.on('error', () => resolve(1));
|
|
1381
|
+
child.on('close', (status) => resolve(status === null ? 1 : status));
|
|
1382
|
+
});
|
|
1383
|
+
process.removeListener('SIGINT', wait);
|
|
1384
|
+
process.removeListener('SIGTERM', wait);
|
|
1385
|
+
|
|
1386
|
+
if (code === 0) for (const line of project.homeDoneLines(plan.into)) console.log(line);
|
|
1387
|
+
return code;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Before the browser: it is a 200MB download on a new machine, and a
|
|
1391
|
+
// recording that cannot be checked afterwards is somebody's five minutes
|
|
1392
|
+
// thrown away. Returns null to carry on recording, or an exit code to stop.
|
|
1393
|
+
async function prepareFolder(url) {
|
|
1394
|
+
const plan = project.setupPlan(USER_DIR);
|
|
1395
|
+
if (plan.action === 'ready') return null;
|
|
1396
|
+
|
|
1397
|
+
const refusing = plan.action === 'refuse';
|
|
1398
|
+
for (const line of project.setupLines(plan, url)) (refusing ? console.error : console.log)(line);
|
|
1399
|
+
if (refusing) return 1;
|
|
1400
|
+
|
|
1401
|
+
// Asked where there is a keyboard. Where there is not, the question's own
|
|
1402
|
+
// default is taken rather than refusing: isTTY is false in plenty of
|
|
1403
|
+
// perfectly good terminals (see noWindowLines), and a command that refuses
|
|
1404
|
+
// there can never work for the person who ran it. The one question whose
|
|
1405
|
+
// default is no - a second project inside somebody's first - stays no.
|
|
1406
|
+
const question = project.setupQuestion(plan);
|
|
1407
|
+
const yes = process.stdin.isTTY
|
|
1408
|
+
? project.answeredYes(await askLine(question.text), question.yesByDefault)
|
|
1409
|
+
: question.yesByDefault;
|
|
1410
|
+
if (!yes) {
|
|
1411
|
+
console.log('');
|
|
1412
|
+
console.log(' Nothing was changed, and nothing was recorded.');
|
|
1413
|
+
console.log('');
|
|
1414
|
+
return 1;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (plan.action === 'home') return await recordFromHome(plan, url);
|
|
1418
|
+
return makeReady(USER_DIR) ? null : 1;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1303
1421
|
async function record(url) {
|
|
1304
1422
|
if (!url) {
|
|
1305
1423
|
console.error('');
|
|
@@ -1320,6 +1438,11 @@ async function record(url) {
|
|
|
1320
1438
|
return 1;
|
|
1321
1439
|
}
|
|
1322
1440
|
|
|
1441
|
+
// After the address is known to answer, so a typo is not the thing that
|
|
1442
|
+
// gets a folder set up; before the browser, which is the expensive part.
|
|
1443
|
+
const setup = await prepareFolder(url);
|
|
1444
|
+
if (setup !== null) return setup;
|
|
1445
|
+
|
|
1323
1446
|
if (!ensureBrowser()) return 1;
|
|
1324
1447
|
|
|
1325
1448
|
const existingBefore = listSpecFiles();
|
|
@@ -1429,20 +1552,10 @@ async function record(url) {
|
|
|
1429
1552
|
// Recordings import the fixture by package name, which only resolves if
|
|
1430
1553
|
// kryptheon is in the user's own node_modules. Running through a bare `npx`
|
|
1431
1554
|
// puts the package somewhere the tests cannot see.
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
1434
|
-
// always resolve itself by name, so require.resolve would answer "yes" even
|
|
1435
|
-
// when the user's tests have no way to find it.
|
|
1555
|
+
// One walk, shared with record's setup, so that record and check can never
|
|
1556
|
+
// disagree about whether a folder is ready.
|
|
1436
1557
|
function fixtureResolvesForUser() {
|
|
1437
|
-
|
|
1438
|
-
for (;;) {
|
|
1439
|
-
if (fs.existsSync(path.join(dir, 'node_modules', 'kryptheon', 'kryptheon-fixture.js'))) {
|
|
1440
|
-
return true;
|
|
1441
|
-
}
|
|
1442
|
-
const parent = path.dirname(dir);
|
|
1443
|
-
if (parent === dir) return false;
|
|
1444
|
-
dir = parent;
|
|
1445
|
-
}
|
|
1558
|
+
return project.kryptheonReachableFrom(USER_DIR);
|
|
1446
1559
|
}
|
|
1447
1560
|
|
|
1448
1561
|
// Only a problem if a recording actually asks for it: specs written against a
|
|
@@ -1956,6 +2069,7 @@ module.exports = {
|
|
|
1956
2069
|
applyRules: applyRules,
|
|
1957
2070
|
rulesBlock: rulesBlock,
|
|
1958
2071
|
record: record,
|
|
2072
|
+
prepareFolder: prepareFolder,
|
|
1959
2073
|
finaliseRecording: finaliseRecording,
|
|
1960
2074
|
takeOutSecrets: takeOutSecrets,
|
|
1961
2075
|
reportSecrets: reportSecrets,
|