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/kryptheon-project.js
CHANGED
|
@@ -23,6 +23,12 @@ const path = require('path');
|
|
|
23
23
|
// stumble into.
|
|
24
24
|
const ESCAPE_HATCH = 'KRYPTHEON_ALLOW_NO_PACKAGE_JSON';
|
|
25
25
|
|
|
26
|
+
// Where `record` keeps recordings for somebody who ran it in their home folder.
|
|
27
|
+
// A new terminal opens there, so for anyone who has only a web address - an
|
|
28
|
+
// app built in Lovable, no source folder anywhere - it is where the command
|
|
29
|
+
// gets typed.
|
|
30
|
+
const HOME_RECORDINGS = 'kryptheon-tests';
|
|
31
|
+
|
|
26
32
|
function hasPackageJson(dir) {
|
|
27
33
|
try {
|
|
28
34
|
return fs.statSync(path.join(dir, 'package.json')).isFile();
|
|
@@ -107,6 +113,13 @@ function inspectProject(dir, env) {
|
|
|
107
113
|
const personal = isHomeOrSystem(dir);
|
|
108
114
|
const base = { dir: dir, packaged: packaged, personal: personal, allowed: allowed };
|
|
109
115
|
|
|
116
|
+
// Somebody whose recordings `record` put in a folder of their own, standing
|
|
117
|
+
// in their home folder again because that is where a new terminal opens.
|
|
118
|
+
// Pointed at by name, rather than told to go and find a project they never
|
|
119
|
+
// had.
|
|
120
|
+
const kept = tidy(dir) === tidy(os.homedir()) ? path.join(path.resolve(dir), HOME_RECORDINGS) : null;
|
|
121
|
+
if (kept && hasPackageJson(kept)) base.recordingsAt = kept;
|
|
122
|
+
|
|
110
123
|
if (allowed) return Object.assign({ ok: true, reason: null }, base);
|
|
111
124
|
|
|
112
125
|
// A home folder is refused even when it has a package.json, and this is not
|
|
@@ -131,6 +144,19 @@ function inspectProject(dir, env) {
|
|
|
131
144
|
function noProjectLines(inspection) {
|
|
132
145
|
const lines = [''];
|
|
133
146
|
|
|
147
|
+
if (inspection.recordingsAt) {
|
|
148
|
+
lines.push(' This is your home folder, not a project.');
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push(' Your recordings are in: ' + inspection.recordingsAt);
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push(' To check them:');
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push(' cd "' + inspection.recordingsAt + '"');
|
|
155
|
+
lines.push(' npx kryptheon check');
|
|
156
|
+
lines.push('');
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
if (inspection.personal) {
|
|
135
161
|
lines.push(' This is your home or a system folder, not a project.');
|
|
136
162
|
lines.push('');
|
|
@@ -163,6 +189,16 @@ function noProjectLines(inspection) {
|
|
|
163
189
|
lines.push(' cd path\\to\\your\\project');
|
|
164
190
|
lines.push(' kryptheon check');
|
|
165
191
|
lines.push('');
|
|
192
|
+
// The person with a web address and no folder at all - an app built in
|
|
193
|
+
// Lovable - has no project to change into, and would be stuck on the line
|
|
194
|
+
// above. Not offered in a system folder: record refuses to set one up.
|
|
195
|
+
if (!inspection.personal || tidy(inspection.dir) === tidy(os.homedir())) {
|
|
196
|
+
lines.push(' No project folder, only a web address? Record from here and it');
|
|
197
|
+
lines.push(' will be set up for you:');
|
|
198
|
+
lines.push('');
|
|
199
|
+
lines.push(' npx kryptheon record https://your-app.example.com');
|
|
200
|
+
lines.push('');
|
|
201
|
+
}
|
|
166
202
|
lines.push(' If you really do keep recordings outside a package, set');
|
|
167
203
|
lines.push(' ' + ESCAPE_HATCH + '=1 and this will run anyway.');
|
|
168
204
|
lines.push('');
|
|
@@ -187,8 +223,296 @@ function noRecordingsLines() {
|
|
|
187
223
|
];
|
|
188
224
|
}
|
|
189
225
|
|
|
226
|
+
// --- Setting a folder up, before recording into it ---------------------------
|
|
227
|
+
//
|
|
228
|
+
// Somebody who built their app in Lovable has an address and nothing else: no
|
|
229
|
+
// source folder, no package.json, nothing to change into. Their recordings
|
|
230
|
+
// still need somewhere to live, `check` refuses a folder with no package.json,
|
|
231
|
+
// and a recording cannot load the fixture unless kryptheon is installed where
|
|
232
|
+
// the recording is.
|
|
233
|
+
//
|
|
234
|
+
// `record` used to find all of that out afterwards. Measured in an empty
|
|
235
|
+
// folder: it started the browser, would have let the person spend five minutes
|
|
236
|
+
// using their app and saved the recording - and then `check`, in that same
|
|
237
|
+
// folder, said "There is no project here". So record now works out what is
|
|
238
|
+
// missing before the browser, which is a 200MB download on a new machine, and
|
|
239
|
+
// offers to add it.
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Whether kryptheon can be loaded by a recording saved in this folder.
|
|
243
|
+
*
|
|
244
|
+
* A filesystem walk, not require.resolve: this file lives inside the kryptheon
|
|
245
|
+
* package, and a package with a name and an "exports" map can always resolve
|
|
246
|
+
* itself by name, so require.resolve would answer "yes" even when the user's
|
|
247
|
+
* recordings have no way to find it.
|
|
248
|
+
*/
|
|
249
|
+
function kryptheonReachableFrom(dir) {
|
|
250
|
+
let current = path.resolve(String(dir || ''));
|
|
251
|
+
if (insideKryptheonItself(current)) return true;
|
|
252
|
+
for (;;) {
|
|
253
|
+
if (fs.existsSync(path.join(current, 'node_modules', 'kryptheon', 'kryptheon-fixture.js'))) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
const parent = path.dirname(current);
|
|
257
|
+
if (parent === current) return false;
|
|
258
|
+
current = parent;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Inside the kryptheon package's own folder - this repository.
|
|
264
|
+
*
|
|
265
|
+
* Node lets a package load itself by its own name, so a recording made here
|
|
266
|
+
* finds the fixture with no node_modules/kryptheon anywhere. Without this,
|
|
267
|
+
* record in this repository offered to npm install kryptheon into kryptheon.
|
|
268
|
+
* Only the nearest package.json counts: that is the package a file belongs to.
|
|
269
|
+
*/
|
|
270
|
+
function insideKryptheonItself(dir) {
|
|
271
|
+
let current = dir;
|
|
272
|
+
for (;;) {
|
|
273
|
+
if (hasPackageJson(current)) {
|
|
274
|
+
// The name alone is not enough: somebody's own app can be called that.
|
|
275
|
+
// The fixture beside it is what makes it this package.
|
|
276
|
+
try {
|
|
277
|
+
return JSON.parse(fs.readFileSync(path.join(current, 'package.json'), 'utf8')).name === 'kryptheon' &&
|
|
278
|
+
fs.existsSync(path.join(current, 'kryptheon-fixture.js'));
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const parent = path.dirname(current);
|
|
284
|
+
if (parent === current) return false;
|
|
285
|
+
current = parent;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Installed in this folder itself, not borrowed from one above it.
|
|
291
|
+
*
|
|
292
|
+
* A folder record sets up gets its own copy even when one is reachable from
|
|
293
|
+
* further up. The copy further up is, on the one machine where this was looked
|
|
294
|
+
* at, a stray install in a home folder - and recordings that lean on that stop
|
|
295
|
+
* working the day somebody tidies it away.
|
|
296
|
+
*/
|
|
297
|
+
function kryptheonInstalledIn(dir) {
|
|
298
|
+
return fs.existsSync(path.join(path.resolve(String(dir || '')), 'node_modules', 'kryptheon', 'kryptheon-fixture.js'));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The nearest folder above this one that is a project.
|
|
303
|
+
*
|
|
304
|
+
* Stops at a home or system folder rather than walking through it: the one
|
|
305
|
+
* package.json most likely to be found up there is the stray one a mistaken
|
|
306
|
+
* install left in a home folder, and that is nobody's project.
|
|
307
|
+
*/
|
|
308
|
+
function projectAbove(dir) {
|
|
309
|
+
let current = path.resolve(String(dir || ''));
|
|
310
|
+
for (;;) {
|
|
311
|
+
const parent = path.dirname(current);
|
|
312
|
+
if (parent === current) return null;
|
|
313
|
+
if (isHomeOrSystem(parent)) return null;
|
|
314
|
+
if (hasPackageJson(parent)) return parent;
|
|
315
|
+
current = parent;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** A folder record set up on an earlier run - its package.json asks for kryptheon. */
|
|
320
|
+
function setUpForKryptheon(dir) {
|
|
321
|
+
try {
|
|
322
|
+
const json = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
323
|
+
return !!((json.devDependencies && json.devDependencies.kryptheon) ||
|
|
324
|
+
(json.dependencies && json.dependencies.kryptheon));
|
|
325
|
+
} catch (err) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isEmptyFolder(dir) {
|
|
331
|
+
try {
|
|
332
|
+
return fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length === 0;
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* What this folder needs before a recording made in it can be checked. Only
|
|
340
|
+
* looks; never writes.
|
|
341
|
+
*
|
|
342
|
+
* ready nothing
|
|
343
|
+
* create no package.json and no project above: make one, then install
|
|
344
|
+
* install a project without kryptheon: install it
|
|
345
|
+
* above no package.json, but a project sits above this folder
|
|
346
|
+
* home a home folder: record in a kryptheon-tests folder inside it
|
|
347
|
+
* refuse a system folder, or a home whose kryptheon-tests is taken
|
|
348
|
+
*/
|
|
349
|
+
function setupPlan(dir, env) {
|
|
350
|
+
const environment = env || process.env;
|
|
351
|
+
const here = path.resolve(String(dir || ''));
|
|
352
|
+
|
|
353
|
+
// Somebody who has said they keep recordings outside a package is left
|
|
354
|
+
// exactly as they were before any of this existed.
|
|
355
|
+
if (String(environment[ESCAPE_HATCH] || '') !== '') return { action: 'ready', dir: here };
|
|
356
|
+
|
|
357
|
+
if (tidy(here) === tidy(os.homedir())) {
|
|
358
|
+
const into = path.join(here, HOME_RECORDINGS);
|
|
359
|
+
// Used only if it is plainly ours or plainly nothing. A folder of that name
|
|
360
|
+
// holding something else is somebody's, and recording into it would put a
|
|
361
|
+
// package.json and node_modules in the middle of it.
|
|
362
|
+
if (!fs.existsSync(into) || isEmptyFolder(into) || setUpForKryptheon(into)) {
|
|
363
|
+
return { action: 'home', dir: here, into: into };
|
|
364
|
+
}
|
|
365
|
+
return { action: 'refuse', reason: 'taken', dir: here, into: into };
|
|
366
|
+
}
|
|
367
|
+
if (isHomeOrSystem(here)) return { action: 'refuse', reason: 'system', dir: here };
|
|
368
|
+
|
|
369
|
+
if (!hasPackageJson(here)) {
|
|
370
|
+
const above = projectAbove(here);
|
|
371
|
+
if (above) return { action: 'above', dir: here, project: above };
|
|
372
|
+
return { action: 'create', dir: here };
|
|
373
|
+
}
|
|
374
|
+
if (!kryptheonReachableFrom(here)) return { action: 'install', dir: here };
|
|
375
|
+
return { action: 'ready', dir: here };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** What is about to happen to this folder, said before it happens. */
|
|
379
|
+
function setupLines(plan, url) {
|
|
380
|
+
const address = url || 'https://your-app.example.com';
|
|
381
|
+
const lines = [''];
|
|
382
|
+
|
|
383
|
+
if (plan.action === 'create') {
|
|
384
|
+
lines.push(' This folder is not set up for recordings yet.');
|
|
385
|
+
lines.push('');
|
|
386
|
+
lines.push(' You are in: ' + plan.dir);
|
|
387
|
+
lines.push('');
|
|
388
|
+
lines.push(' Your recordings are saved here, and kryptheon has to be installed');
|
|
389
|
+
lines.push(' beside them or they cannot be checked later. So I will add:');
|
|
390
|
+
lines.push('');
|
|
391
|
+
lines.push(' package.json a small file that marks this folder as a project');
|
|
392
|
+
lines.push(' node_modules kryptheon itself, about 20MB');
|
|
393
|
+
lines.push('');
|
|
394
|
+
lines.push(' Nothing already in this folder is touched.');
|
|
395
|
+
} else if (plan.action === 'install') {
|
|
396
|
+
lines.push(' kryptheon is not installed in this project yet.');
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push(' You are in: ' + plan.dir);
|
|
399
|
+
lines.push('');
|
|
400
|
+
lines.push(' Recordings load it by name, so without it here they cannot be');
|
|
401
|
+
lines.push(' checked later. I will run:');
|
|
402
|
+
lines.push('');
|
|
403
|
+
lines.push(' npm install --save-dev kryptheon');
|
|
404
|
+
lines.push('');
|
|
405
|
+
lines.push(' which adds one line to package.json and about 20MB to node_modules.');
|
|
406
|
+
} else if (plan.action === 'above') {
|
|
407
|
+
lines.push(' This folder is inside a project:');
|
|
408
|
+
lines.push('');
|
|
409
|
+
lines.push(' ' + plan.project);
|
|
410
|
+
lines.push('');
|
|
411
|
+
lines.push(' Recordings belong beside the app they test, so the usual thing is');
|
|
412
|
+
lines.push(' to run this from there:');
|
|
413
|
+
lines.push('');
|
|
414
|
+
lines.push(' cd "' + plan.project + '"');
|
|
415
|
+
lines.push(' npx kryptheon record ' + address);
|
|
416
|
+
lines.push('');
|
|
417
|
+
lines.push(' If that is not your project - a package.json can be left behind by');
|
|
418
|
+
lines.push(' an install run in the wrong place - I can set this folder up on its');
|
|
419
|
+
lines.push(' own instead.');
|
|
420
|
+
} else if (plan.action === 'home') {
|
|
421
|
+
lines.push(' You are in your home folder.');
|
|
422
|
+
lines.push('');
|
|
423
|
+
lines.push(' Recordings are kept in a folder of their own rather than loose in');
|
|
424
|
+
lines.push(' here, so I will ' + (fs.existsSync(plan.into) ? 'use this one' : 'make one') + ' and work in that:');
|
|
425
|
+
lines.push('');
|
|
426
|
+
lines.push(' ' + plan.into);
|
|
427
|
+
} else if (plan.action === 'refuse' && plan.reason === 'taken') {
|
|
428
|
+
lines.push(' You are in your home folder. Recordings are kept in a folder of');
|
|
429
|
+
lines.push(' their own, and I would normally use:');
|
|
430
|
+
lines.push('');
|
|
431
|
+
lines.push(' ' + plan.into);
|
|
432
|
+
lines.push('');
|
|
433
|
+
lines.push(' but something else is already there, and it is not one I made.');
|
|
434
|
+
lines.push(' Nothing was changed.');
|
|
435
|
+
lines.push('');
|
|
436
|
+
lines.push(' Make a new folder of your own, open a terminal in it, and run the');
|
|
437
|
+
lines.push(' same command again.');
|
|
438
|
+
} else if (plan.action === 'refuse') {
|
|
439
|
+
lines.push(' This is a system folder, not somewhere to keep recordings.');
|
|
440
|
+
lines.push('');
|
|
441
|
+
lines.push(' You are in: ' + plan.dir);
|
|
442
|
+
lines.push('');
|
|
443
|
+
lines.push(' Nothing was changed. Make a new folder of your own - on the Desktop');
|
|
444
|
+
lines.push(' is fine - open a terminal in it, and run the same command again.');
|
|
445
|
+
if (process.platform === 'win32') {
|
|
446
|
+
lines.push(' (In File Explorer: right-click inside the folder, Open in Terminal.)');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
lines.push('');
|
|
450
|
+
return lines;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The question, and which way Enter answers it.
|
|
455
|
+
*
|
|
456
|
+
* Yes by default where saying yes is what the person came for. No by default
|
|
457
|
+
* for a folder inside another project, where yes puts a second project inside
|
|
458
|
+
* somebody's first.
|
|
459
|
+
*/
|
|
460
|
+
function setupQuestion(plan) {
|
|
461
|
+
if (plan.action === 'above') return { text: ' Set this folder up on its own instead? [y/N] ', yesByDefault: false };
|
|
462
|
+
if (plan.action === 'install') return { text: ' Install it? [Y/n] ', yesByDefault: true };
|
|
463
|
+
if (plan.action === 'home') return { text: ' Go ahead? [Y/n] ', yesByDefault: true };
|
|
464
|
+
return { text: ' Set this folder up? [Y/n] ', yesByDefault: true };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** An answer typed at the question, read the way the question offered it. */
|
|
468
|
+
function answeredYes(answer, yesByDefault) {
|
|
469
|
+
const typed = String(answer || '').trim();
|
|
470
|
+
if (!typed) return !!yesByDefault;
|
|
471
|
+
return /^y/i.test(typed);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** After recording from a home folder: where the recordings went, and how to get back to them. */
|
|
475
|
+
function homeDoneLines(into) {
|
|
476
|
+
return [
|
|
477
|
+
'',
|
|
478
|
+
' Your recordings are in: ' + into,
|
|
479
|
+
'',
|
|
480
|
+
' To check them after a change, open a terminal and run:',
|
|
481
|
+
'',
|
|
482
|
+
' cd "' + into + '"',
|
|
483
|
+
' npx kryptheon check',
|
|
484
|
+
'',
|
|
485
|
+
];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** The install did not finish. Nothing needs undoing: running again picks up where this stopped. */
|
|
489
|
+
function installFailedLines() {
|
|
490
|
+
return [
|
|
491
|
+
'',
|
|
492
|
+
' kryptheon could not be installed in this folder.',
|
|
493
|
+
'',
|
|
494
|
+
' Installing needs an internet connection. Check it, then run the same',
|
|
495
|
+
' command again - nothing needs undoing first.',
|
|
496
|
+
'',
|
|
497
|
+
' Or install it yourself, then record:',
|
|
498
|
+
'',
|
|
499
|
+
' npm install --save-dev kryptheon',
|
|
500
|
+
'',
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
|
|
190
504
|
module.exports = {
|
|
191
505
|
ESCAPE_HATCH: ESCAPE_HATCH,
|
|
506
|
+
HOME_RECORDINGS: HOME_RECORDINGS,
|
|
507
|
+
kryptheonReachableFrom: kryptheonReachableFrom,
|
|
508
|
+
kryptheonInstalledIn: kryptheonInstalledIn,
|
|
509
|
+
projectAbove: projectAbove,
|
|
510
|
+
setupPlan: setupPlan,
|
|
511
|
+
setupLines: setupLines,
|
|
512
|
+
setupQuestion: setupQuestion,
|
|
513
|
+
answeredYes: answeredYes,
|
|
514
|
+
homeDoneLines: homeDoneLines,
|
|
515
|
+
installFailedLines: installFailedLines,
|
|
192
516
|
hasPackageJson: hasPackageJson,
|
|
193
517
|
systemFolders: systemFolders,
|
|
194
518
|
tidy: tidy,
|
package/kryptheon-reporter.js
CHANGED
|
@@ -170,6 +170,94 @@ function firstMeaningfulLine(message) {
|
|
|
170
170
|
return line ? line.replace(/^(TimeoutError|Error):\s*/, '').trim() : 'The test failed.';
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Errors that belong to no test.
|
|
175
|
+
//
|
|
176
|
+
// A spec file with a syntax error, or one importing something that is not
|
|
177
|
+
// there, never becomes a test at all - so nothing calls onTestEnd and the run
|
|
178
|
+
// ends with no results. Playwright reports it through onError. A reporter that
|
|
179
|
+
// does not implement onError prints a summary of nothing and exits 1, and the
|
|
180
|
+
// person is left with a silent failure and no idea which file is at fault.
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
// Playwright says this once the load errors have already been reported. On its
|
|
184
|
+
// own it means an empty tests folder; after a real error it is a consequence,
|
|
185
|
+
// not a cause, and repeating it just buries the thing that has to be fixed.
|
|
186
|
+
function isNoTestsFound(error) {
|
|
187
|
+
return /No tests found/i.test(String((error && error.message) || ''));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A load error, reduced to the file, the line and one sentence.
|
|
192
|
+
*
|
|
193
|
+
* The message carries a code frame in ANSI colour and repeats the absolute
|
|
194
|
+
* path inside its own first line. Neither helps here: the path is printed
|
|
195
|
+
* separately and relative, and the frame belongs in an editor.
|
|
196
|
+
*/
|
|
197
|
+
function readLoadError(error, userDir) {
|
|
198
|
+
const raw = stripAnsi((error && error.message) || (error && error.value) || String(error || ""));
|
|
199
|
+
const location = (error && error.location) || {};
|
|
200
|
+
|
|
201
|
+
let file = location.file || null;
|
|
202
|
+
if (file) {
|
|
203
|
+
try {
|
|
204
|
+
file = path.relative(userDir || process.cwd(), file);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
/* an absolute path is still better than nothing */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// First line only, with the absolute path taken back out of it.
|
|
211
|
+
let message = raw.split("\n").map(function (l) { return l.trim(); }).filter(Boolean)[0] || "The file could not be read.";
|
|
212
|
+
if (location.file) message = message.split(location.file + ": ").join("");
|
|
213
|
+
message = message.replace(/\s*\(\d+:\d+\)\s*$/, "");
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
file: file,
|
|
217
|
+
line: typeof location.line === "number" ? location.line : null,
|
|
218
|
+
message: message,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* What to print when nothing could be loaded.
|
|
224
|
+
*
|
|
225
|
+
* Said as "this file", not "the tests failed", because nothing was tested. The
|
|
226
|
+
* app may be perfectly fine; a file in the tests folder is not.
|
|
227
|
+
*/
|
|
228
|
+
function loadErrorLines(errors, userDir) {
|
|
229
|
+
const real = (errors || []).filter(function (e) { return !isNoTestsFound(e); });
|
|
230
|
+
const lines = [];
|
|
231
|
+
|
|
232
|
+
if (!real.length) {
|
|
233
|
+
// Nothing ran and nothing explained why. Saying so is still better than a
|
|
234
|
+
// summary of zero, which reads as "everything is fine".
|
|
235
|
+
lines.push("Nothing ran.");
|
|
236
|
+
lines.push("");
|
|
237
|
+
lines.push("No recording produced a result, and the test runner gave no reason.");
|
|
238
|
+
lines.push("Check that the files in tests/ contain a test.");
|
|
239
|
+
lines.push("");
|
|
240
|
+
return lines;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const many = real.length > 1;
|
|
244
|
+
lines.push(many ? real.length + " of your recordings could not be read:" : "One of your recordings could not be read:");
|
|
245
|
+
lines.push("");
|
|
246
|
+
for (const error of real) {
|
|
247
|
+
const read = readLoadError(error, userDir);
|
|
248
|
+
const where = read.file ? read.file + (read.line ? " line " + read.line : "") : "(the file was not named)";
|
|
249
|
+
lines.push(" " + where);
|
|
250
|
+
lines.push(" " + read.message);
|
|
251
|
+
lines.push("");
|
|
252
|
+
}
|
|
253
|
+
lines.push(many
|
|
254
|
+
? "Nothing else ran, because those files could not be loaded at all."
|
|
255
|
+
: "Nothing else ran, because that file could not be loaded at all.");
|
|
256
|
+
lines.push("Fix it and run this again.");
|
|
257
|
+
lines.push("");
|
|
258
|
+
return lines;
|
|
259
|
+
}
|
|
260
|
+
|
|
173
261
|
// ---------------------------------------------------------------------------
|
|
174
262
|
// Translation: Playwright error -> { reason, advice }
|
|
175
263
|
// ---------------------------------------------------------------------------
|
|
@@ -575,10 +663,19 @@ class KryptheonReporter {
|
|
|
575
663
|
this.previousRuns = [];
|
|
576
664
|
this.records = [];
|
|
577
665
|
this.startedAt = new Date();
|
|
666
|
+
// Errors that belong to no test: a spec file that would not load.
|
|
667
|
+
this.loadErrors = [];
|
|
578
668
|
// Set by any test whose page never said what encoding it was in.
|
|
579
669
|
this.sawGuessedEncoding = false;
|
|
580
670
|
}
|
|
581
671
|
|
|
672
|
+
// Playwright calls this for anything that happened outside a test: a spec
|
|
673
|
+
// file that would not load, most often. Kept rather than printed here, so
|
|
674
|
+
// the reason lands beside the summary instead of ahead of the run.
|
|
675
|
+
onError(error) {
|
|
676
|
+
this.loadErrors.push(error);
|
|
677
|
+
}
|
|
678
|
+
|
|
582
679
|
onBegin() {
|
|
583
680
|
this.startedAt = new Date();
|
|
584
681
|
// Read history before this run is appended, so "was working on" looks
|
|
@@ -807,10 +904,35 @@ class KryptheonReporter {
|
|
|
807
904
|
// has ever had a baseline - says the app is at fault when nothing is.
|
|
808
905
|
const counts = recordings.summarise(this.records);
|
|
809
906
|
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
907
|
+
// A file that would not load is said first, and always - before the quiet
|
|
908
|
+
// shortcut, and whatever else the run managed to do.
|
|
909
|
+
//
|
|
910
|
+
// In practice a file that will not load stops the whole run, so nothing
|
|
911
|
+
// else has anything to report and the block below returns. This is not
|
|
912
|
+
// written as though that were guaranteed: onError also carries errors that
|
|
913
|
+
// arrive outside any test, and one of those turning up after results would
|
|
914
|
+
// otherwise be dropped in silence - which is the bug this whole thing is
|
|
915
|
+
// here to fix.
|
|
916
|
+
const unreadable = this.loadErrors.some(function (e) { return !isNoTestsFound(e); });
|
|
917
|
+
if (unreadable) {
|
|
918
|
+
process.stdout.write(
|
|
919
|
+
String.fromCharCode(10) + loadErrorLines(this.loadErrors, USER_DIR).join(String.fromCharCode(10)),
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Nothing ran at all. A summary of zero reads as good news, and in quiet
|
|
924
|
+
// mode it would say "0 recordings still working" over the top of a file
|
|
925
|
+
// that does not compile, so neither is printed. If the reason has not
|
|
926
|
+
// already been given above, it is given now.
|
|
927
|
+
if (!this.records.length) {
|
|
928
|
+
if (!unreadable) {
|
|
929
|
+
process.stdout.write(
|
|
930
|
+
String.fromCharCode(10) + loadErrorLines(this.loadErrors, USER_DIR).join(String.fromCharCode(10)),
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
814
936
|
if (QUIET && !counts.broken && !counts.unproven) {
|
|
815
937
|
const s = counts.working === 1 ? '' : 's';
|
|
816
938
|
process.stdout.write('OK ' + counts.working + ' recording' + s + ' still working.\n');
|
|
@@ -838,3 +960,7 @@ module.exports = KryptheonReporter;
|
|
|
838
960
|
// Shared with the fixture so baselines strip hosts exactly the same way.
|
|
839
961
|
// Attached to the exported class, so `new (require(...))()` keeps working.
|
|
840
962
|
module.exports.requestPath = requestPath;
|
|
963
|
+
|
|
964
|
+
// Exported for the checks: what a file that would not load turns into.
|
|
965
|
+
module.exports.readLoadError = readLoadError;
|
|
966
|
+
module.exports.loadErrorLines = loadErrorLines;
|