kryptheon 0.1.35 → 0.1.37
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 +4 -0
- package/bin/kryptheon.js +93 -8
- package/kryptheon-code.js +35 -3
- package/kryptheon-dashboard-page.js +10 -1
- package/kryptheon-dashboard.js +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,6 +70,10 @@ npx kryptheon record https://your-app.example.com
|
|
|
70
70
|
A browser opens. Use your app the way a customer would. Close the browser when
|
|
71
71
|
you are done, and the recording is saved as a test.
|
|
72
72
|
|
|
73
|
+
Add `--phone` to record in a phone-sized window (390 x 844) - for a menu or a
|
|
74
|
+
layout that only changes on a phone. The recording keeps that size, so every
|
|
75
|
+
replay uses it too. On the dashboard it is the "Phone size" box under Record.
|
|
76
|
+
|
|
73
77
|
On its own, a recording knows that every button and field is still there and
|
|
74
78
|
that the last page looks the same. To say what must be true — "Reservation
|
|
75
79
|
received" appears after booking — use the toolbar at the top of the page while
|
package/bin/kryptheon.js
CHANGED
|
@@ -91,6 +91,7 @@ function usage() {
|
|
|
91
91
|
console.log(' The same, for scripts and AI tools:');
|
|
92
92
|
console.log('');
|
|
93
93
|
console.log(' npx kryptheon record <url> open your app and record what you do as a test');
|
|
94
|
+
console.log(' ... <url> --phone the same in a phone-sized window, replayed at that size');
|
|
94
95
|
console.log(' npx kryptheon check run every recorded test and report in plain language');
|
|
95
96
|
console.log(' npx kryptheon accept <name> agree that one test\'s new result is the correct one');
|
|
96
97
|
console.log(' npx kryptheon remove [name] list the recordings, or remove one you no longer want');
|
|
@@ -1527,7 +1528,28 @@ async function prepareFolder(url) {
|
|
|
1527
1528
|
return makeReady(USER_DIR) ? null : 1;
|
|
1528
1529
|
}
|
|
1529
1530
|
|
|
1530
|
-
|
|
1531
|
+
// A phone's window: the size the recording is made at and replayed at.
|
|
1532
|
+
// Found on a blind test (Northstar): a mobile menu that never opened and a
|
|
1533
|
+
// card wider than the screen were only visible below 440px, and every
|
|
1534
|
+
// recording was made at desktop width.
|
|
1535
|
+
// Written with a comma: codegen's own help shows "1280x720", but codegen
|
|
1536
|
+
// itself refuses that ("use width,height") - measured on Playwright 1.62.
|
|
1537
|
+
const PHONE_SIZE = '390,844';
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* What codegen is started with. At phone size, codegen writes
|
|
1541
|
+
* test.use({ viewport: ... }) into the recording itself, so every replay
|
|
1542
|
+
* opens at the size it was recorded at - nothing else has to remember it.
|
|
1543
|
+
*/
|
|
1544
|
+
function codegenArgs(outFile, target, options) {
|
|
1545
|
+
const phone = options && options.phone;
|
|
1546
|
+
return ['codegen', '--target', 'playwright-test']
|
|
1547
|
+
.concat(phone ? ['--viewport-size=' + PHONE_SIZE] : [])
|
|
1548
|
+
.concat(['-o', outFile, target]);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
async function record(url, options) {
|
|
1552
|
+
const phone = Boolean(options && options.phone);
|
|
1531
1553
|
if (!url) {
|
|
1532
1554
|
console.error('');
|
|
1533
1555
|
console.error(' Which address should I open?');
|
|
@@ -1571,7 +1593,8 @@ async function record(url) {
|
|
|
1571
1593
|
const specPath = path.join(USER_DIR, outFile);
|
|
1572
1594
|
|
|
1573
1595
|
console.log('');
|
|
1574
|
-
console.log(' Opening ' + target + ' in a browser.');
|
|
1596
|
+
console.log(' Opening ' + target + (phone ? ' in a phone-sized window (' + PHONE_SIZE.replace(',', ' x ') + ').' : ' in a browser.'));
|
|
1597
|
+
if (phone) console.log(' It is replayed at the same size every time.');
|
|
1575
1598
|
console.log('');
|
|
1576
1599
|
console.log(' Use your app normally - click, type, sign in, whatever you want');
|
|
1577
1600
|
console.log(' covered. Every step is recorded as you go.');
|
|
@@ -1579,7 +1602,7 @@ async function record(url) {
|
|
|
1579
1602
|
console.log(' When you are done, close the browser window to save the test.');
|
|
1580
1603
|
console.log('');
|
|
1581
1604
|
|
|
1582
|
-
const child = startCodegen(
|
|
1605
|
+
const child = startCodegen(codegenArgs(outFile, target, { phone: phone }));
|
|
1583
1606
|
if (!child) return 1;
|
|
1584
1607
|
|
|
1585
1608
|
let stderr = '';
|
|
@@ -2450,10 +2473,11 @@ function recheckFromDashboard(id, opened, say) {
|
|
|
2450
2473
|
}
|
|
2451
2474
|
|
|
2452
2475
|
/** Records one flow the way `npx kryptheon record <address>` does, and names what it saved. */
|
|
2453
|
-
function recordFromDashboard(address, say) {
|
|
2476
|
+
function recordFromDashboard(address, say, options) {
|
|
2477
|
+
const phone = Boolean(options && options.phone);
|
|
2454
2478
|
const before = listSpecFiles();
|
|
2455
|
-
say('A browser window opens on ' + address + '. Use your app, then close that window to save.');
|
|
2456
|
-
return kryptheonChild(['record', address], say).then(({ code }) => {
|
|
2479
|
+
say('A ' + (phone ? 'phone-sized ' : '') + 'browser window opens on ' + address + '. Use your app, then close that window to save.');
|
|
2480
|
+
return kryptheonChild(['record', address].concat(phone ? ['--phone'] : []), say).then(({ code }) => {
|
|
2457
2481
|
const added = listSpecFiles().filter((name) => !before.includes(name));
|
|
2458
2482
|
if (!added.length) return { ok: false, exit: code, why: 'no recording was saved' };
|
|
2459
2483
|
return { ok: true, exit: code, recordings: added, summary: 'saved ' + added.join(', ') };
|
|
@@ -2539,9 +2563,68 @@ function dashboardAlreadyRunning(port) {
|
|
|
2539
2563
|
});
|
|
2540
2564
|
}
|
|
2541
2565
|
|
|
2566
|
+
/**
|
|
2567
|
+
* The dashboard, started from the home folder: kept in a kryptheon-tests
|
|
2568
|
+
* folder there, the one record already uses - set up the first time, asked
|
|
2569
|
+
* once, and simply opened every time after.
|
|
2570
|
+
*
|
|
2571
|
+
* Found when the first person paid: the site's Kryptheon.cmd download was
|
|
2572
|
+
* flagged as dangerous by the browser and by Windows, and would not open
|
|
2573
|
+
* once saved. Its replacement is Win+R and one pasted line - and Win+R starts
|
|
2574
|
+
* in the home folder, where dashboard used to refuse to run at all.
|
|
2575
|
+
*/
|
|
2576
|
+
async function dashboardFromHome(plan) {
|
|
2577
|
+
const ready = project.hasPackageJson(plan.into) && project.kryptheonInstalledIn(plan.into);
|
|
2578
|
+
if (!ready) {
|
|
2579
|
+
console.log('');
|
|
2580
|
+
console.log(' Kryptheon keeps your recordings and results in a folder of their own:');
|
|
2581
|
+
console.log('');
|
|
2582
|
+
console.log(' ' + plan.into);
|
|
2583
|
+
console.log('');
|
|
2584
|
+
console.log(' The first time, I make it and install kryptheon in it (about 20MB).');
|
|
2585
|
+
console.log(' Nothing else on this computer is touched.');
|
|
2586
|
+
console.log('');
|
|
2587
|
+
const question = project.setupQuestion(plan);
|
|
2588
|
+
const yes = process.stdin.isTTY
|
|
2589
|
+
? project.answeredYes(await askLine(question.text), question.yesByDefault)
|
|
2590
|
+
: question.yesByDefault;
|
|
2591
|
+
if (!yes) {
|
|
2592
|
+
console.log('');
|
|
2593
|
+
console.log(' Nothing was changed. To keep them beside your app instead, open a');
|
|
2594
|
+
console.log(' terminal in its folder and run: npx kryptheon@latest dashboard');
|
|
2595
|
+
console.log('');
|
|
2596
|
+
return 1;
|
|
2597
|
+
}
|
|
2598
|
+
try {
|
|
2599
|
+
fs.mkdirSync(plan.into, { recursive: true });
|
|
2600
|
+
} catch (err) {
|
|
2601
|
+
console.error('');
|
|
2602
|
+
console.error(' Could not make the folder: ' + plan.into);
|
|
2603
|
+
console.error('');
|
|
2604
|
+
return 1;
|
|
2605
|
+
}
|
|
2606
|
+
if (!makeReady(plan.into)) return 1;
|
|
2607
|
+
}
|
|
2608
|
+
// Run again from inside that folder, as record does: every path in this
|
|
2609
|
+
// file is worked out once, from the folder the command started in.
|
|
2610
|
+
const child = spawn(process.execPath, [__filename, 'dashboard'], { cwd: plan.into, stdio: 'inherit' });
|
|
2611
|
+
const wait = () => {};
|
|
2612
|
+
process.on('SIGINT', wait);
|
|
2613
|
+
process.on('SIGTERM', wait);
|
|
2614
|
+
const code = await new Promise((resolve) => {
|
|
2615
|
+
child.on('error', () => resolve(1));
|
|
2616
|
+
child.on('close', (status) => resolve(status === null ? 1 : status));
|
|
2617
|
+
});
|
|
2618
|
+
process.removeListener('SIGINT', wait);
|
|
2619
|
+
process.removeListener('SIGTERM', wait);
|
|
2620
|
+
return code;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2542
2623
|
async function dashboardCommand() {
|
|
2543
2624
|
const here = project.inspectProject(USER_DIR);
|
|
2544
2625
|
if (!here.ok) {
|
|
2626
|
+
const plan = project.setupPlan(USER_DIR);
|
|
2627
|
+
if (plan.action === 'home') return await dashboardFromHome(plan);
|
|
2545
2628
|
for (const line of project.noProjectLines(here)) console.error(line);
|
|
2546
2629
|
return 1;
|
|
2547
2630
|
}
|
|
@@ -2565,7 +2648,7 @@ async function dashboardCommand() {
|
|
|
2565
2648
|
const app = dash.createServer(USER_DIR, {
|
|
2566
2649
|
run: (id, say) => runFromDashboard(id, opened, say),
|
|
2567
2650
|
recheck: (id, say) => recheckFromDashboard(id, opened, say),
|
|
2568
|
-
record: (address, say) => recordFromDashboard(address, say),
|
|
2651
|
+
record: (address, say, options) => recordFromDashboard(address, say, options),
|
|
2569
2652
|
db: (action, connection, say) => databaseFromDashboard(action, connection, say),
|
|
2570
2653
|
words: (connection) => nightWords(connection),
|
|
2571
2654
|
});
|
|
@@ -2867,7 +2950,7 @@ async function main() {
|
|
|
2867
2950
|
|
|
2868
2951
|
switch (command) {
|
|
2869
2952
|
case 'record':
|
|
2870
|
-
return finishWith(await record(rest
|
|
2953
|
+
return finishWith(await record(rest.find((a) => !/^--/.test(a)), { phone: rest.includes('--phone') }));
|
|
2871
2954
|
break;
|
|
2872
2955
|
case 'check': {
|
|
2873
2956
|
const handedOver = handToFolderCopy();
|
|
@@ -2919,6 +3002,8 @@ async function main() {
|
|
|
2919
3002
|
|
|
2920
3003
|
// Exported so the naming logic can be checked without opening a browser.
|
|
2921
3004
|
module.exports = {
|
|
3005
|
+
codegenArgs: codegenArgs,
|
|
3006
|
+
PHONE_SIZE: PHONE_SIZE,
|
|
2922
3007
|
packageVersion: packageVersion,
|
|
2923
3008
|
humanisePath: humanisePath,
|
|
2924
3009
|
nameFromTitle: nameFromTitle,
|
package/kryptheon-code.js
CHANGED
|
@@ -137,8 +137,11 @@ function calleeName(node) {
|
|
|
137
137
|
// feeds another. It was 6, which a constant-only lookup (a template, a call,
|
|
138
138
|
// its return, a map of maps) used up before reaching the strings at the
|
|
139
139
|
// bottom, so a harmless label came back "unknown". Giving up always errs
|
|
140
|
-
// towards reporting, never towards silence.
|
|
141
|
-
|
|
140
|
+
// towards reporting, never towards silence. It was 12 until a blind test
|
|
141
|
+
// (Northstar): a helper's parameter, back to its call inside a map() callback,
|
|
142
|
+
// to the list, a slice of a list reassigned from a filter of the fixed one -
|
|
143
|
+
// 13 steps. 18 leaves room for one more layer of the same.
|
|
144
|
+
const MAX_DEPTH = 18;
|
|
142
145
|
|
|
143
146
|
function classify(node, scope, code, depth, aliases) {
|
|
144
147
|
if (!node || depth > MAX_DEPTH) return 'unknown';
|
|
@@ -483,7 +486,7 @@ function fromBinding(id, scope, code, depth, aliases) {
|
|
|
483
486
|
const binding = scope.getBinding(id.name);
|
|
484
487
|
if (!binding) return 'unknown';
|
|
485
488
|
if (binding.path && binding.path.type === 'CatchClause') return 'error';
|
|
486
|
-
if (binding.kind === 'param') return fromCalls(binding, code, depth);
|
|
489
|
+
if (binding.kind === 'param') return fromCallback(binding, code, depth) || fromCalls(binding, code, depth);
|
|
487
490
|
if (binding.kind === 'module') return fromImport(binding, depth);
|
|
488
491
|
const init = binding.path && binding.path.node && binding.path.node.init;
|
|
489
492
|
const later = binding.constantViolations || [];
|
|
@@ -518,6 +521,35 @@ function fromBinding(id, scope, code, depth, aliases) {
|
|
|
518
521
|
return classify(init, binding.path.scope, code, depth + 1, aliases);
|
|
519
522
|
}
|
|
520
523
|
|
|
524
|
+
// Array methods that hand their callback the list's own items.
|
|
525
|
+
const ITEM_CALLBACKS = /^(map|forEach|filter|find|findLast|findIndex|findLastIndex|some|every|flatMap|sort|toSorted)$/;
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* A parameter of a callback handed to list.map(...) and its kin: the item is
|
|
529
|
+
* whatever the list holds, the index a number, the third the list itself.
|
|
530
|
+
* Null when the parameter is not that.
|
|
531
|
+
*
|
|
532
|
+
* Found on a blind test (Northstar): pageRows.map(row => `...${severityClass(row.severity)}...`).
|
|
533
|
+
* The helper's parameter was followed to its call, and there `row` was a
|
|
534
|
+
* parameter of an arrow nobody names - "unknown" - though pageRows only ever
|
|
535
|
+
* held the file's own fixed list. The map().join() reading knew what row was;
|
|
536
|
+
* a name followed back into the same callback did not.
|
|
537
|
+
*/
|
|
538
|
+
function fromCallback(binding, code, depth) {
|
|
539
|
+
const fn = binding.path && binding.path.parentPath;
|
|
540
|
+
if (!fn || !/Function/.test(fn.node.type)) return null;
|
|
541
|
+
const call = fn.parentPath;
|
|
542
|
+
if (!call || !/CallExpression$/.test(call.node.type) || call.node.arguments[0] !== fn.node) return null;
|
|
543
|
+
const method = calleeName(call.node.callee);
|
|
544
|
+
if (!/MemberExpression$/.test(call.node.callee.type) || !ITEM_CALLBACKS.test(method)) return null;
|
|
545
|
+
const index = fn.node.params.indexOf(binding.path.node);
|
|
546
|
+
const list = () => classify(call.node.callee.object, call.scope, code, depth + 1);
|
|
547
|
+
if (index === 0 || (index === 1 && /^(sort|toSorted)$/.test(method))) return list();
|
|
548
|
+
if (index === 1) return 'safe';
|
|
549
|
+
if (index === 2) return list();
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
521
553
|
/**
|
|
522
554
|
* A parameter is as dangerous as the worst thing any call passes it. Every
|
|
523
555
|
* use of the function has to be a call this file makes: one handed on as a
|
|
@@ -155,6 +155,10 @@
|
|
|
155
155
|
recordInput.placeholder = 'http://localhost:3000';
|
|
156
156
|
recordInput.setAttribute('aria-label', 'Your app address');
|
|
157
157
|
recordInput.spellcheck = false;
|
|
158
|
+
// Kept like the box beside it, so a page refresh does not untick it.
|
|
159
|
+
var phoneBox = el('input');
|
|
160
|
+
phoneBox.type = 'checkbox';
|
|
161
|
+
phoneBox.id = 'recordPhone';
|
|
158
162
|
var dbInput = el('input', 'field');
|
|
159
163
|
dbInput.type = 'password';
|
|
160
164
|
dbInput.placeholder = 'postgresql://postgres.<project>:<password>@...pooler.supabase.com:5432/postgres';
|
|
@@ -441,7 +445,7 @@
|
|
|
441
445
|
var go = button('sun sm', 'film', a.label, function () {
|
|
442
446
|
var address = recordInput.value.trim();
|
|
443
447
|
if (!address) { toast('Put your app\'s address in the box first - for example http://localhost:3000'); recordInput.focus(); return; }
|
|
444
|
-
runJob('/api/record', { url: address }).then(function (done) {
|
|
448
|
+
runJob('/api/record', { url: address, phone: phoneBox.checked }).then(function (done) {
|
|
445
449
|
var r = done && done.result;
|
|
446
450
|
toast(r && r.ok ? 'Saved: ' + (r.recordings || []).join(', ') : 'Nothing was saved' + (r && r.why ? ' - ' + r.why : ''));
|
|
447
451
|
}, function () {});
|
|
@@ -450,6 +454,11 @@
|
|
|
450
454
|
go.disabled = busy;
|
|
451
455
|
line.appendChild(go);
|
|
452
456
|
wrap.appendChild(line);
|
|
457
|
+
var size = el('label', 'fallback');
|
|
458
|
+
size.setAttribute('for', 'recordPhone');
|
|
459
|
+
size.appendChild(phoneBox);
|
|
460
|
+
size.appendChild(document.createTextNode(' Phone size (390 x 844) - for menus and layouts that only change on a phone. Replayed at the same size.'));
|
|
461
|
+
wrap.appendChild(size);
|
|
453
462
|
wrap.appendChild(el('div', 'fallback', 'or in a terminal: ' + a.command));
|
|
454
463
|
return wrap;
|
|
455
464
|
}
|
package/kryptheon-dashboard.js
CHANGED
|
@@ -825,7 +825,9 @@ function createServer(root, options) {
|
|
|
825
825
|
if (url.pathname === '/api/record' && typeof opts.record === 'function') {
|
|
826
826
|
const address = appAddress(input.url);
|
|
827
827
|
if (!address) return json(res, 400, { ok: false, why: 'that is not a web address - it should look like http://localhost:3000' });
|
|
828
|
-
|
|
828
|
+
const phone = input.phone === true;
|
|
829
|
+
return begin('record', 'Recording ' + address + (phone ? ' at phone size' : ''),
|
|
830
|
+
'npx kryptheon record ' + address + (phone ? ' --phone' : ''), (say) => opts.record(address, say, { phone: phone }));
|
|
829
831
|
}
|
|
830
832
|
if (url.pathname === '/api/db' && typeof opts.db === 'function') {
|
|
831
833
|
const action = String(input.action || '');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kryptheon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "Record what you do in your app and get plain-language reports when it breaks.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@playwright/test": "^1.62.1"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
|
-
"check": "node kryptheon-store.check.js && node kryptheon-code.check.js && node kryptheon-constdata.check.js && node kryptheon-accept.check.js && node kryptheon-keys.check.js && node kryptheon-dashboard.check.js && node kryptheon-autostart.check.js && node kryptheon-changes.check.js && node kryptheon-look.check.js && node kryptheon-verify.check.js && node kryptheon-recheck.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",
|
|
55
|
+
"check": "node kryptheon-store.check.js && node kryptheon-code.check.js && node kryptheon-constdata.check.js && node kryptheon-accept.check.js && node kryptheon-keys.check.js && node kryptheon-dashboard.check.js && node kryptheon-autostart.check.js && node kryptheon-changes.check.js && node kryptheon-look.check.js && node kryptheon-verify.check.js && node kryptheon-recheck.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-phone.check.js && node kryptheon-homedash.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",
|
|
56
56
|
"check:store": "node kryptheon-store.check.js",
|
|
57
57
|
"check:code": "node kryptheon-code.check.js",
|
|
58
58
|
"check:constdata": "node kryptheon-constdata.check.js",
|