kryptheon 0.1.34 → 0.1.36
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/LICENSE +38 -0
- package/README.md +5 -1
- package/bin/kryptheon.js +35 -9
- package/kryptheon-code.js +35 -3
- package/kryptheon-dashboard-page.js +10 -1
- package/kryptheon-dashboard.js +3 -1
- package/package.json +4 -11
package/LICENSE
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Kryptheon Software License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kryptheon (https://kryptheon.tech). All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is proprietary. It is not open source.
|
|
6
|
+
|
|
7
|
+
1. Use. You may install and run this software, as published by Kryptheon,
|
|
8
|
+
free of charge, to check software and databases that you own or are
|
|
9
|
+
authorised to test. Features that kryptheon.tech offers on a paid plan
|
|
10
|
+
are provided only under that plan and the Terms at
|
|
11
|
+
https://kryptheon.tech/terms.
|
|
12
|
+
|
|
13
|
+
2. Restrictions. Without Kryptheon's written permission you may not:
|
|
14
|
+
(a) copy, modify, translate or create derivative works of this software,
|
|
15
|
+
except as needed to run it;
|
|
16
|
+
(b) sell, sublicense, rent, publish or otherwise distribute it, or offer
|
|
17
|
+
it, or anything derived from it, as a hosted or managed service;
|
|
18
|
+
(c) remove, disable or work around any licence, plan or usage limit, or
|
|
19
|
+
any part of the software that enforces them;
|
|
20
|
+
(d) use it, or any part of it, to build a competing product.
|
|
21
|
+
|
|
22
|
+
3. No warranty. The software is provided "as is", without warranty of any
|
|
23
|
+
kind, express or implied, including fitness for a particular purpose.
|
|
24
|
+
It may miss problems; a check that finds nothing does not mean that
|
|
25
|
+
nothing is wrong. You are responsible for what you test and for keeping
|
|
26
|
+
your own backups.
|
|
27
|
+
|
|
28
|
+
4. Limitation of liability. To the extent the law allows, Kryptheon is not
|
|
29
|
+
liable for any damages arising from the use of, or inability to use,
|
|
30
|
+
this software.
|
|
31
|
+
|
|
32
|
+
5. Earlier versions. Versions published before this licence was added keep
|
|
33
|
+
the licence they were published with.
|
|
34
|
+
|
|
35
|
+
6. Termination. This licence ends automatically if you break it. When it
|
|
36
|
+
ends, you must stop using the software and delete your copies.
|
|
37
|
+
|
|
38
|
+
Questions: company@kryptheon.tech
|
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
|
|
@@ -254,4 +258,4 @@ rather than refusing up front.
|
|
|
254
258
|
|
|
255
259
|
## Licence
|
|
256
260
|
|
|
257
|
-
|
|
261
|
+
Free to use, not open source: see LICENSE. Paid features are on kryptheon.tech.
|
package/bin/kryptheon.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
// The kryptheon CLI. A thin wrapper so nobody has to remember Playwright's
|
|
3
3
|
// command line or edit a config file. Everything here shells out to the
|
|
4
4
|
// Playwright binary that ships with this package.
|
|
@@ -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(', ') };
|
|
@@ -2565,7 +2589,7 @@ async function dashboardCommand() {
|
|
|
2565
2589
|
const app = dash.createServer(USER_DIR, {
|
|
2566
2590
|
run: (id, say) => runFromDashboard(id, opened, say),
|
|
2567
2591
|
recheck: (id, say) => recheckFromDashboard(id, opened, say),
|
|
2568
|
-
record: (address, say) => recordFromDashboard(address, say),
|
|
2592
|
+
record: (address, say, options) => recordFromDashboard(address, say, options),
|
|
2569
2593
|
db: (action, connection, say) => databaseFromDashboard(action, connection, say),
|
|
2570
2594
|
words: (connection) => nightWords(connection),
|
|
2571
2595
|
});
|
|
@@ -2867,7 +2891,7 @@ async function main() {
|
|
|
2867
2891
|
|
|
2868
2892
|
switch (command) {
|
|
2869
2893
|
case 'record':
|
|
2870
|
-
return finishWith(await record(rest
|
|
2894
|
+
return finishWith(await record(rest.find((a) => !/^--/.test(a)), { phone: rest.includes('--phone') }));
|
|
2871
2895
|
break;
|
|
2872
2896
|
case 'check': {
|
|
2873
2897
|
const handedOver = handToFolderCopy();
|
|
@@ -2919,6 +2943,8 @@ async function main() {
|
|
|
2919
2943
|
|
|
2920
2944
|
// Exported so the naming logic can be checked without opening a browser.
|
|
2921
2945
|
module.exports = {
|
|
2946
|
+
codegenArgs: codegenArgs,
|
|
2947
|
+
PHONE_SIZE: PHONE_SIZE,
|
|
2922
2948
|
packageVersion: packageVersion,
|
|
2923
2949
|
humanisePath: humanisePath,
|
|
2924
2950
|
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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kryptheon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "Record what you do in your app and get plain-language reports when it breaks.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"bin": {
|
|
8
8
|
"kryptheon": "bin/kryptheon.js"
|
|
@@ -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-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",
|
|
@@ -89,12 +89,5 @@
|
|
|
89
89
|
"end-to-end",
|
|
90
90
|
"cli"
|
|
91
91
|
],
|
|
92
|
-
"
|
|
93
|
-
"type": "git",
|
|
94
|
-
"url": "git+https://github.com/jhashantanu987-beep/kryptheon-cli.git"
|
|
95
|
-
},
|
|
96
|
-
"bugs": {
|
|
97
|
-
"url": "https://github.com/jhashantanu987-beep/kryptheon-cli/issues"
|
|
98
|
-
},
|
|
99
|
-
"homepage": "https://github.com/jhashantanu987-beep/kryptheon-cli#readme"
|
|
92
|
+
"homepage": "https://kryptheon.tech"
|
|
100
93
|
}
|