kryptheon 0.1.1 → 0.1.2
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/bin/kryptheon.js +371 -67
- package/package.json +1 -1
package/bin/kryptheon.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const { spawnSync } = require('child_process');
|
|
13
|
+
const { spawn, spawnSync } = require('child_process');
|
|
14
14
|
|
|
15
15
|
const PACKAGE_DIR = path.join(__dirname, '..');
|
|
16
16
|
const USER_DIR = process.cwd();
|
|
@@ -19,6 +19,10 @@ const TESTS_DIR = path.join(USER_DIR, 'tests');
|
|
|
19
19
|
|
|
20
20
|
const MIN_NODE = [20, 6, 0];
|
|
21
21
|
|
|
22
|
+
// How long to wait for a browser window to appear before giving up.
|
|
23
|
+
const WINDOW_POLL_MS = 3000;
|
|
24
|
+
const WINDOW_TIMEOUT_MS = 30000;
|
|
25
|
+
|
|
22
26
|
function nodeIsTooOld(version) {
|
|
23
27
|
const parts = String(version || process.versions.node).split('.').map(Number);
|
|
24
28
|
for (let i = 0; i < MIN_NODE.length; i++) {
|
|
@@ -133,6 +137,43 @@ function listSpecFiles() {
|
|
|
133
137
|
.filter((name) => /\.(spec|test)\.(c|m)?[jt]sx?$/.test(name));
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
// A plain GET on Node's own http/https, deliberately NOT fetch. fetch keeps
|
|
141
|
+
// its sockets alive in a pool, and those were still open at exit - handles
|
|
142
|
+
// being torn down while the process is ending is how libuv assertions happen
|
|
143
|
+
// on Windows. agent:false closes the socket as soon as we are done with it.
|
|
144
|
+
function httpGet(url, timeoutMs, redirectsLeft) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
let parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = new URL(url);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return reject(Object.assign(new Error('bad address'), { code: 'ERR_INVALID_URL' }));
|
|
151
|
+
}
|
|
152
|
+
const lib = parsed.protocol === 'https:' ? require('https') : require('http');
|
|
153
|
+
const req = lib.get(url, { agent: false, timeout: timeoutMs || 8000 }, (res) => {
|
|
154
|
+
const location = res.headers && res.headers.location;
|
|
155
|
+
if (location && res.statusCode >= 300 && res.statusCode < 400 && (redirectsLeft || 0) > 0) {
|
|
156
|
+
res.resume();
|
|
157
|
+
req.destroy();
|
|
158
|
+
return resolve(httpGet(new URL(location, url).toString(), timeoutMs, redirectsLeft - 1));
|
|
159
|
+
}
|
|
160
|
+
let body = '';
|
|
161
|
+
res.setEncoding('utf8');
|
|
162
|
+
res.on('data', (chunk) => {
|
|
163
|
+
if (body.length < 300000) body += chunk;
|
|
164
|
+
});
|
|
165
|
+
res.on('end', () => {
|
|
166
|
+
req.destroy();
|
|
167
|
+
resolve({ status: res.statusCode, body: body });
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
req.on('timeout', () => {
|
|
171
|
+
req.destroy(Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }));
|
|
172
|
+
});
|
|
173
|
+
req.on('error', reject);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
136
177
|
// --- is the address actually there? ----------------------------------------
|
|
137
178
|
|
|
138
179
|
// "localhost:3000" is what people type; give it a scheme before using it.
|
|
@@ -177,10 +218,7 @@ function classifyFetchError(err) {
|
|
|
177
218
|
|
|
178
219
|
async function reachability(url, timeoutMs) {
|
|
179
220
|
try {
|
|
180
|
-
|
|
181
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs || 8000);
|
|
182
|
-
await fetch(url, { signal: controller.signal, redirect: 'manual' });
|
|
183
|
-
clearTimeout(timer);
|
|
221
|
+
await httpGet(url, timeoutMs || 8000, 0);
|
|
184
222
|
return { ok: true, kind: 'reachable' };
|
|
185
223
|
} catch (err) {
|
|
186
224
|
const kind = classifyFetchError(err);
|
|
@@ -251,6 +289,19 @@ function countRecordedActions(source) {
|
|
|
251
289
|
return found ? found.length : 0;
|
|
252
290
|
}
|
|
253
291
|
|
|
292
|
+
// A spec that pulls Playwright in directly gets none of our diagnostics.
|
|
293
|
+
function importsPlaywrightDirectly(source) {
|
|
294
|
+
const text = String(source || '');
|
|
295
|
+
// The literal specifier is what matters; import and require both carry it.
|
|
296
|
+
return text.indexOf("'@playwright/test'") !== -1 || text.indexOf('"@playwright/test"') !== -1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// codegen always writes the opening page.goto. A recording is only real if
|
|
300
|
+
// something else happened as well.
|
|
301
|
+
function isRealRecording(source) {
|
|
302
|
+
return countRecordedActions(source) >= 2;
|
|
303
|
+
}
|
|
304
|
+
|
|
254
305
|
function timestampName() {
|
|
255
306
|
const d = new Date();
|
|
256
307
|
const pad = (n) => String(n).padStart(2, '0');
|
|
@@ -324,13 +375,9 @@ function nameFromTitle(title) {
|
|
|
324
375
|
// to the recorded path if this cannot be reached.
|
|
325
376
|
async function fetchTitle(url) {
|
|
326
377
|
try {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
clearTimeout(timer);
|
|
331
|
-
if (!res.ok) return null;
|
|
332
|
-
const html = await res.text();
|
|
333
|
-
const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
378
|
+
const res = await httpGet(url, 5000, 2);
|
|
379
|
+
if (!res || res.status < 200 || res.status >= 300) return null;
|
|
380
|
+
const m = String(res.body).match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
334
381
|
return m ? m[1].replace(/\s+/g, ' ').trim() : null;
|
|
335
382
|
} catch (err) {
|
|
336
383
|
return null;
|
|
@@ -420,6 +467,127 @@ async function nameRecording(relativeFile) {
|
|
|
420
467
|
}
|
|
421
468
|
return target;
|
|
422
469
|
}
|
|
470
|
+
// Recording needs a real browser window on someone's screen. Inside an AI
|
|
471
|
+
// coding assistant there is no desktop to draw it on, so codegen would sit
|
|
472
|
+
// there forever and the run would be killed before anything was tidied up.
|
|
473
|
+
function looksLikeNoDesktop() {
|
|
474
|
+
if (process.env.KRYPTHEON_FORCE_RECORD) return false;
|
|
475
|
+
return !process.stdout.isTTY;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function explainNoWindow(url) {
|
|
479
|
+
console.error('');
|
|
480
|
+
console.error(' Recording needs a real browser window on your screen.');
|
|
481
|
+
console.error('');
|
|
482
|
+
console.error(' This terminal cannot show one - that is what happens inside an');
|
|
483
|
+
console.error(' AI coding assistant, or any window-less session.');
|
|
484
|
+
console.error('');
|
|
485
|
+
console.error(' Open Command Prompt (or PowerShell) yourself and run:');
|
|
486
|
+
console.error(' npx kryptheon record ' + url);
|
|
487
|
+
console.error('');
|
|
488
|
+
console.error(' Nothing was recorded.');
|
|
489
|
+
console.error('');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Is a Playwright-controlled browser actually showing a window?
|
|
493
|
+
function browserWindowIsUp() {
|
|
494
|
+
try {
|
|
495
|
+
if (process.platform === 'win32') {
|
|
496
|
+
const probe = spawnSync(
|
|
497
|
+
'powershell',
|
|
498
|
+
[
|
|
499
|
+
'-NoProfile',
|
|
500
|
+
'-Command',
|
|
501
|
+
"@(Get-Process chrome,msedge -ErrorAction SilentlyContinue | " +
|
|
502
|
+
"Where-Object { $_.Path -like '*ms-playwright*' -and $_.MainWindowHandle -ne 0 }).Count",
|
|
503
|
+
],
|
|
504
|
+
{ encoding: 'utf8', windowsHide: true, timeout: 8000 }
|
|
505
|
+
);
|
|
506
|
+
return Number(String(probe.stdout || '').trim()) > 0;
|
|
507
|
+
}
|
|
508
|
+
const probe = spawnSync('ps', ['-A', '-o', 'command'], { encoding: 'utf8', timeout: 8000 });
|
|
509
|
+
return /ms-playwright/.test(String(probe.stdout || ''));
|
|
510
|
+
} catch (err) {
|
|
511
|
+
return true; // cannot tell - do not block the user
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Runs codegen without blocking the event loop, so SIGINT and SIGTERM can
|
|
516
|
+
// still be handled. spawnSync would freeze the loop and no handler would run.
|
|
517
|
+
function startCodegen(args) {
|
|
518
|
+
const cli = findPlaywrightCli();
|
|
519
|
+
if (!cli) {
|
|
520
|
+
console.error('');
|
|
521
|
+
console.error(' The testing engine is missing from this install.');
|
|
522
|
+
console.error(' Reinstalling usually fixes it: npm install -g kryptheon');
|
|
523
|
+
console.error('');
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
return spawn(process.execPath, [cli].concat(args), {
|
|
527
|
+
cwd: USER_DIR,
|
|
528
|
+
stdio: ['inherit', 'inherit', 'pipe'],
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Everything that has to happen to whatever codegen left behind, however
|
|
533
|
+
// the run ended. Module level so it can be exercised directly.
|
|
534
|
+
async function finaliseRecording(outFile, context) {
|
|
535
|
+
const ctx = context || {};
|
|
536
|
+
const specPath = path.join(USER_DIR, outFile);
|
|
537
|
+
let source = null;
|
|
538
|
+
try {
|
|
539
|
+
source = fs.readFileSync(specPath, 'utf8');
|
|
540
|
+
} catch (err) {
|
|
541
|
+
source = null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (source !== null && isRealRecording(source)) {
|
|
545
|
+
// Import rewrite first: it is instant, so even a second interruption
|
|
546
|
+
// leaves behind a spec that works.
|
|
547
|
+
pointAtFixture(outFile);
|
|
548
|
+
const named = await nameRecording(outFile);
|
|
549
|
+
console.log('');
|
|
550
|
+
console.log(' Saved to ' + named);
|
|
551
|
+
console.log(' Run it any time with: kryptheon check');
|
|
552
|
+
console.log('');
|
|
553
|
+
return { code: 0, savedAs: named };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (source !== null) {
|
|
557
|
+
try {
|
|
558
|
+
fs.unlinkSync(specPath);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
/* nothing more we can do */
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (!ctx.hadTestsDir) {
|
|
564
|
+
try {
|
|
565
|
+
if (fs.readdirSync(TESTS_DIR).length === 0) fs.rmdirSync(TESTS_DIR);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
/* only tidying up */
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (ctx.reason === 'no-window') {
|
|
572
|
+
explainNoWindow(ctx.target);
|
|
573
|
+
} else if (ctx.reason === 'signal') {
|
|
574
|
+
console.log('');
|
|
575
|
+
console.log(' Recording stopped before anything was saved.');
|
|
576
|
+
console.log('');
|
|
577
|
+
console.log(' Nothing was recorded.');
|
|
578
|
+
console.log('');
|
|
579
|
+
} else if (source === null || /ERR_|Error:/.test(String(ctx.stderr || ''))) {
|
|
580
|
+
explainCodegenFailure(ctx.target, ctx.stderr);
|
|
581
|
+
} else {
|
|
582
|
+
console.log('');
|
|
583
|
+
console.log(' Nothing was recorded.');
|
|
584
|
+
console.log('');
|
|
585
|
+
console.log(' The browser closed before anything was clicked or typed.');
|
|
586
|
+
console.log(' Run the same command again and use your app before closing it.');
|
|
587
|
+
console.log('');
|
|
588
|
+
}
|
|
589
|
+
return { code: 1, savedAs: null };
|
|
590
|
+
}
|
|
423
591
|
|
|
424
592
|
async function record(url) {
|
|
425
593
|
if (!url) {
|
|
@@ -431,8 +599,16 @@ async function record(url) {
|
|
|
431
599
|
console.error('');
|
|
432
600
|
return 1;
|
|
433
601
|
}
|
|
602
|
+
|
|
434
603
|
const target = normaliseRecordUrl(url);
|
|
435
604
|
|
|
605
|
+
// No desktop means no window means nothing to record. Say so now rather
|
|
606
|
+
// than hanging until something kills us.
|
|
607
|
+
if (looksLikeNoDesktop()) {
|
|
608
|
+
explainNoWindow(target);
|
|
609
|
+
return 1;
|
|
610
|
+
}
|
|
611
|
+
|
|
436
612
|
// Fail early and kindly rather than letting codegen throw a stack trace.
|
|
437
613
|
const reach = await reachability(target);
|
|
438
614
|
if (!reach.ok) {
|
|
@@ -449,7 +625,8 @@ async function record(url) {
|
|
|
449
625
|
/* codegen will report if it cannot write */
|
|
450
626
|
}
|
|
451
627
|
|
|
452
|
-
|
|
628
|
+
const outFile = path.join('tests', timestampName());
|
|
629
|
+
const specPath = path.join(USER_DIR, outFile);
|
|
453
630
|
|
|
454
631
|
console.log('');
|
|
455
632
|
console.log(' Opening ' + target + ' in a browser.');
|
|
@@ -460,58 +637,74 @@ async function record(url) {
|
|
|
460
637
|
console.log(' When you are done, close the browser window to save the test.');
|
|
461
638
|
console.log('');
|
|
462
639
|
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
console.log('');
|
|
485
|
-
return 0;
|
|
486
|
-
}
|
|
640
|
+
const child = startCodegen(['codegen', '--target', 'playwright-test', '-o', outFile, target]);
|
|
641
|
+
if (!child) return 1;
|
|
642
|
+
|
|
643
|
+
let stderr = '';
|
|
644
|
+
if (child.stderr) child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
645
|
+
|
|
646
|
+
// Everything that has to happen no matter how this ends: tidy the file,
|
|
647
|
+
// wire it up, name it. Runs once, whether codegen exits on its own or we
|
|
648
|
+
// are interrupted.
|
|
649
|
+
let finished = false;
|
|
650
|
+
const finalise = async (reason) => {
|
|
651
|
+
if (finished) return 1;
|
|
652
|
+
finished = true;
|
|
653
|
+
const outcome = await finaliseRecording(outFile, {
|
|
654
|
+
hadTestsDir: hadTestsDir,
|
|
655
|
+
target: target,
|
|
656
|
+
stderr: stderr,
|
|
657
|
+
reason: reason,
|
|
658
|
+
});
|
|
659
|
+
return outcome.code;
|
|
660
|
+
};
|
|
487
661
|
|
|
488
|
-
|
|
489
|
-
if (source !== null) {
|
|
662
|
+
const stopChild = () => {
|
|
490
663
|
try {
|
|
491
|
-
|
|
664
|
+
child.kill();
|
|
492
665
|
} catch (err) {
|
|
493
|
-
/*
|
|
666
|
+
/* already gone */
|
|
494
667
|
}
|
|
495
|
-
}
|
|
496
|
-
if (!hadTestsDir) {
|
|
497
|
-
try {
|
|
498
|
-
if (fs.readdirSync(TESTS_DIR).length === 0) fs.rmdirSync(TESTS_DIR);
|
|
499
|
-
} catch (err) {
|
|
500
|
-
/* only tidying up */
|
|
501
|
-
}
|
|
502
|
-
}
|
|
668
|
+
};
|
|
503
669
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
670
|
+
return await new Promise((resolve) => {
|
|
671
|
+
// If no window ever appears, stop instead of waiting for someone to kill us.
|
|
672
|
+
let waited = 0;
|
|
673
|
+
const watchdog = setInterval(() => {
|
|
674
|
+
waited += WINDOW_POLL_MS;
|
|
675
|
+
if (browserWindowIsUp()) {
|
|
676
|
+
clearInterval(watchdog);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (waited >= WINDOW_TIMEOUT_MS) {
|
|
680
|
+
clearInterval(watchdog);
|
|
681
|
+
stopChild();
|
|
682
|
+
finalise('no-window').then(resolve);
|
|
683
|
+
}
|
|
684
|
+
}, WINDOW_POLL_MS);
|
|
685
|
+
|
|
686
|
+
const onSignal = () => {
|
|
687
|
+
clearInterval(watchdog);
|
|
688
|
+
stopChild();
|
|
689
|
+
finalise('signal').then(resolve);
|
|
690
|
+
};
|
|
691
|
+
process.once('SIGINT', onSignal);
|
|
692
|
+
process.once('SIGTERM', onSignal);
|
|
693
|
+
process.once('SIGHUP', onSignal);
|
|
694
|
+
|
|
695
|
+
child.on('error', () => {
|
|
696
|
+
clearInterval(watchdog);
|
|
697
|
+
finalise('error').then(resolve);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
child.on('close', () => {
|
|
701
|
+
clearInterval(watchdog);
|
|
702
|
+
process.removeListener('SIGINT', onSignal);
|
|
703
|
+
process.removeListener('SIGTERM', onSignal);
|
|
704
|
+
process.removeListener('SIGHUP', onSignal);
|
|
705
|
+
finalise('exit').then(resolve);
|
|
706
|
+
});
|
|
707
|
+
});
|
|
515
708
|
}
|
|
516
709
|
|
|
517
710
|
// Recordings import the fixture by package name, which only resolves if
|
|
@@ -545,6 +738,46 @@ function specsNeedThePackage() {
|
|
|
545
738
|
});
|
|
546
739
|
}
|
|
547
740
|
|
|
741
|
+
// Older versions, and any run that was killed part way, left specs behind that
|
|
742
|
+
// import Playwright directly (so they produce no diagnostics) or that contain
|
|
743
|
+
// nothing but the opening navigation (so they verify nothing). Repair the
|
|
744
|
+
// first kind and refuse to run the second, rather than reporting a false OK.
|
|
745
|
+
function triageSpecs() {
|
|
746
|
+
const repaired = [];
|
|
747
|
+
const notRecordings = [];
|
|
748
|
+
const runnable = [];
|
|
749
|
+
|
|
750
|
+
for (const name of listSpecFiles()) {
|
|
751
|
+
const full = path.join(TESTS_DIR, name);
|
|
752
|
+
let source;
|
|
753
|
+
try {
|
|
754
|
+
source = fs.readFileSync(full, 'utf8');
|
|
755
|
+
} catch (err) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (/(from|require\()\s*(['"])@playwright\/test\2/.test(source)) {
|
|
760
|
+
const relative = path.join('tests', name);
|
|
761
|
+
if (pointAtFixture(relative)) {
|
|
762
|
+
repaired.push(name);
|
|
763
|
+
try {
|
|
764
|
+
source = fs.readFileSync(full, 'utf8');
|
|
765
|
+
} catch (err) {
|
|
766
|
+
/* keep what we had */
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (!isRealRecording(source)) {
|
|
772
|
+
notRecordings.push(name);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
runnable.push(name);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return { repaired: repaired, notRecordings: notRecordings, runnable: runnable };
|
|
779
|
+
}
|
|
780
|
+
|
|
548
781
|
function check() {
|
|
549
782
|
if (!listSpecFiles().length) {
|
|
550
783
|
console.log('');
|
|
@@ -570,8 +803,50 @@ function check() {
|
|
|
570
803
|
console.log('');
|
|
571
804
|
return 1;
|
|
572
805
|
}
|
|
806
|
+
const triage = triageSpecs();
|
|
807
|
+
|
|
808
|
+
for (const name of triage.repaired) {
|
|
809
|
+
console.log('');
|
|
810
|
+
console.log(' Repaired ' + path.join('tests', name));
|
|
811
|
+
console.log(' It was saved without the part that explains failures. Fixed now.');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
for (const name of triage.notRecordings) {
|
|
815
|
+
console.log('');
|
|
816
|
+
console.log(' Skipped ' + path.join('tests', name));
|
|
817
|
+
console.log(' This is not a real recording - it only opens a page and stops,');
|
|
818
|
+
console.log(' so it cannot tell you whether anything works.');
|
|
819
|
+
console.log(' Record it again: kryptheon record <url>');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!triage.runnable.length) {
|
|
823
|
+
console.log('');
|
|
824
|
+
console.log(' Nothing could be checked.');
|
|
825
|
+
console.log('');
|
|
826
|
+
console.log(' None of the files in tests/ is a usable recording.');
|
|
827
|
+
console.log(' Record one with: kryptheon record <url>');
|
|
828
|
+
console.log('');
|
|
829
|
+
return 1;
|
|
830
|
+
}
|
|
831
|
+
|
|
573
832
|
if (!ensureBrowser()) return 1;
|
|
574
|
-
|
|
833
|
+
|
|
834
|
+
// Only the usable recordings are handed to the test runner, so a file that
|
|
835
|
+
// cannot produce diagnostics can never be counted as passing.
|
|
836
|
+
const only = triage.runnable.map((name) => path.join('tests', name).split(path.sep).join('/'));
|
|
837
|
+
const status = runPlaywright(['test', '--config', CONFIG].concat(only)).status;
|
|
838
|
+
|
|
839
|
+
// Without this, a green summary could still hide a recording that checked
|
|
840
|
+
// nothing at all.
|
|
841
|
+
if (triage.notRecordings.length) {
|
|
842
|
+
const many = triage.notRecordings.length > 1;
|
|
843
|
+
console.log(
|
|
844
|
+
' Note: ' + triage.notRecordings.length + ' recording' + (many ? 's' : '') +
|
|
845
|
+
' above checked nothing and ' + (many ? 'were' : 'was') + ' skipped.'
|
|
846
|
+
);
|
|
847
|
+
console.log('');
|
|
848
|
+
}
|
|
849
|
+
return status;
|
|
575
850
|
}
|
|
576
851
|
|
|
577
852
|
// --- accept -----------------------------------------------------------------
|
|
@@ -676,6 +951,29 @@ function accept(name) {
|
|
|
676
951
|
|
|
677
952
|
const [command, ...rest] = process.argv.slice(2);
|
|
678
953
|
|
|
954
|
+
// process.exit() cuts the process off mid-teardown. On Windows a terminal's
|
|
955
|
+
// output is written asynchronously, so exiting while the last line is still
|
|
956
|
+
// queued tears down a handle that is already closing - which is what produces
|
|
957
|
+
// "Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)" in libuv. Setting
|
|
958
|
+
// the code and letting Node wind down on its own avoids that entirely, and
|
|
959
|
+
// still reports the same exit status.
|
|
960
|
+
async function finishWith(code) {
|
|
961
|
+
process.exitCode = code;
|
|
962
|
+
const streams = [process.stdout, process.stderr];
|
|
963
|
+
await Promise.all(
|
|
964
|
+
streams.map(
|
|
965
|
+
(stream) =>
|
|
966
|
+
new Promise((resolve) => {
|
|
967
|
+
if (!stream || typeof stream.write !== 'function' || !stream.writableLength) return resolve();
|
|
968
|
+
const done = () => resolve();
|
|
969
|
+
stream.once('drain', done);
|
|
970
|
+
const guard = setTimeout(done, 1000);
|
|
971
|
+
if (guard.unref) guard.unref();
|
|
972
|
+
})
|
|
973
|
+
)
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
679
977
|
async function main() {
|
|
680
978
|
if (nodeIsTooOld()) {
|
|
681
979
|
console.error('');
|
|
@@ -685,31 +983,31 @@ async function main() {
|
|
|
685
983
|
console.error('');
|
|
686
984
|
console.error(' Download the latest from https://nodejs.org and try again.');
|
|
687
985
|
console.error('');
|
|
688
|
-
|
|
986
|
+
return finishWith(1);
|
|
689
987
|
}
|
|
690
988
|
|
|
691
989
|
switch (command) {
|
|
692
990
|
case 'record':
|
|
693
|
-
|
|
991
|
+
return finishWith(await record(rest[0]));
|
|
694
992
|
break;
|
|
695
993
|
case 'check':
|
|
696
|
-
|
|
994
|
+
return finishWith(check());
|
|
697
995
|
break;
|
|
698
996
|
case 'accept':
|
|
699
|
-
|
|
997
|
+
return finishWith(accept(rest.join(' ').trim()));
|
|
700
998
|
break;
|
|
701
999
|
case undefined:
|
|
702
1000
|
case '-h':
|
|
703
1001
|
case '--help':
|
|
704
1002
|
case 'help':
|
|
705
1003
|
usage();
|
|
706
|
-
|
|
1004
|
+
return finishWith(0);
|
|
707
1005
|
break;
|
|
708
1006
|
default:
|
|
709
1007
|
console.error('');
|
|
710
1008
|
console.error(' Unknown command: ' + command);
|
|
711
1009
|
usage();
|
|
712
|
-
|
|
1010
|
+
return finishWith(1);
|
|
713
1011
|
}
|
|
714
1012
|
}
|
|
715
1013
|
|
|
@@ -723,6 +1021,12 @@ module.exports = {
|
|
|
723
1021
|
pointAtFixture: pointAtFixture,
|
|
724
1022
|
listSpecFiles: listSpecFiles,
|
|
725
1023
|
countRecordedActions: countRecordedActions,
|
|
1024
|
+
isRealRecording: isRealRecording,
|
|
1025
|
+
importsPlaywrightDirectly: importsPlaywrightDirectly,
|
|
1026
|
+
triageSpecs: triageSpecs,
|
|
1027
|
+
record: record,
|
|
1028
|
+
finaliseRecording: finaliseRecording,
|
|
1029
|
+
looksLikeNoDesktop: looksLikeNoDesktop,
|
|
726
1030
|
classifyFetchError: classifyFetchError,
|
|
727
1031
|
normaliseRecordUrl: normaliseRecordUrl,
|
|
728
1032
|
isLocalAddress: isLocalAddress,
|