quickpickle 1.6.2 → 1.7.0
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 +79 -6
- package/dist/index.cjs +208 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.esm.js +207 -27
- package/dist/index.esm.js.map +1 -1
- package/dist/models/DataTable.d.ts +138 -0
- package/dist/steps.d.ts +1 -1
- package/dist/world.d.ts +1 -0
- package/package.json +19 -20
package/dist/index.esm.js
CHANGED
|
@@ -3,8 +3,6 @@ import { intersection, isFunction, isString, concat, fromPairs, escapeRegExp, de
|
|
|
3
3
|
import parse from '@cucumber/tag-expressions';
|
|
4
4
|
import * as Gherkin from '@cucumber/gherkin';
|
|
5
5
|
import * as Messages from '@cucumber/messages';
|
|
6
|
-
import { DataTable } from '@cucumber/cucumber';
|
|
7
|
-
export { DataTable } from '@cucumber/cucumber';
|
|
8
6
|
|
|
9
7
|
const steps = [];
|
|
10
8
|
const parameterTypeRegistry = new ParameterTypeRegistry();
|
|
@@ -433,6 +431,169 @@ function explodeTags(explodeTags, testTags) {
|
|
|
433
431
|
return combined.length ? combined.map(arr => [...tagsToTest, ...arr]) : [testTags];
|
|
434
432
|
}
|
|
435
433
|
|
|
434
|
+
class DataTable {
|
|
435
|
+
constructor(sourceTable) {
|
|
436
|
+
if (sourceTable instanceof Array) {
|
|
437
|
+
this.rawTable = sourceTable;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
this.rawTable = sourceTable.rows.map((row) => row.cells.map((cell) => cell.value));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* This method returns an array of objects of the shape { [key: string]: string }.
|
|
445
|
+
* It is intended for tables with a header row, as follows:
|
|
446
|
+
*
|
|
447
|
+
* ```
|
|
448
|
+
* | id | name | color | taste |
|
|
449
|
+
* | 1 | apple | red | sweet |
|
|
450
|
+
* | 2 | banana | yellow | sweet |
|
|
451
|
+
* | 3 | orange | orange | sour |
|
|
452
|
+
* ```
|
|
453
|
+
*
|
|
454
|
+
* This would return the following array of objects:
|
|
455
|
+
*
|
|
456
|
+
* ```
|
|
457
|
+
* [
|
|
458
|
+
* { id: '1', name: 'apple', color: 'red', taste: 'sweet' },
|
|
459
|
+
* { id: '2', name: 'banana', color: 'yellow', taste: 'sweet' },
|
|
460
|
+
* { id: '3', name: 'orange', color: 'orange', taste: 'sour' },
|
|
461
|
+
* ]
|
|
462
|
+
* ```
|
|
463
|
+
*
|
|
464
|
+
* @returns Record<string, string>[]
|
|
465
|
+
*/
|
|
466
|
+
hashes() {
|
|
467
|
+
const copy = this.raw();
|
|
468
|
+
const keys = copy[0];
|
|
469
|
+
const valuesArray = copy.slice(1);
|
|
470
|
+
return valuesArray.map((values) => {
|
|
471
|
+
const rowObject = {};
|
|
472
|
+
keys.forEach((key, index) => (rowObject[key] = values[index]));
|
|
473
|
+
return rowObject;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* This method returns the raw table as a two-dimensional array.
|
|
478
|
+
* It can be used for tables with or without a header row, for example:
|
|
479
|
+
*
|
|
480
|
+
* ```
|
|
481
|
+
* | id | name | color | taste |
|
|
482
|
+
* | 1 | apple | red | sweet |
|
|
483
|
+
* | 2 | banana | yellow | sweet |
|
|
484
|
+
* | 3 | orange | orange | sour |
|
|
485
|
+
* ```
|
|
486
|
+
*
|
|
487
|
+
* would return the following array of objects:
|
|
488
|
+
*
|
|
489
|
+
* ```
|
|
490
|
+
* [
|
|
491
|
+
* ['id', 'name', 'color', 'taste'],
|
|
492
|
+
* ['1', 'apple', 'red', 'sweet'],
|
|
493
|
+
* ['2', 'banana', 'yellow', 'sweet'],
|
|
494
|
+
* ['3', 'orange', 'orange', 'sour'],
|
|
495
|
+
* ]
|
|
496
|
+
* ```
|
|
497
|
+
*
|
|
498
|
+
* @returns string[][]
|
|
499
|
+
*/
|
|
500
|
+
raw() {
|
|
501
|
+
return this.rawTable.slice(0);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* This method is intended for tables with a header row, and returns
|
|
505
|
+
* the value rows as a two-dimensional array, without the header row:
|
|
506
|
+
*
|
|
507
|
+
* ```
|
|
508
|
+
* | id | name | color | taste |
|
|
509
|
+
* | 1 | apple | red | sweet |
|
|
510
|
+
* | 2 | banana | yellow | sweet |
|
|
511
|
+
* | 3 | orange | orange | sour |
|
|
512
|
+
* ```
|
|
513
|
+
*
|
|
514
|
+
* would return the following array of objects:
|
|
515
|
+
*
|
|
516
|
+
* ```
|
|
517
|
+
* [
|
|
518
|
+
* ['1', 'apple', 'red', 'sweet'],
|
|
519
|
+
* ['2', 'banana', 'yellow', 'sweet'],
|
|
520
|
+
* ['3', 'orange', 'orange', 'sour'],
|
|
521
|
+
* ]
|
|
522
|
+
* ```
|
|
523
|
+
*
|
|
524
|
+
* @returns string[][]
|
|
525
|
+
*/
|
|
526
|
+
rows() {
|
|
527
|
+
const copy = this.raw();
|
|
528
|
+
copy.shift();
|
|
529
|
+
return copy;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* This method is intended for tables with exactly two columns.
|
|
533
|
+
* It returns a single object with the first column as keys and
|
|
534
|
+
* the second column as values.
|
|
535
|
+
*
|
|
536
|
+
* ```
|
|
537
|
+
* | id | 1 |
|
|
538
|
+
* | name | apple |
|
|
539
|
+
* | color | red |
|
|
540
|
+
* | taste | sweet |
|
|
541
|
+
* ```
|
|
542
|
+
*
|
|
543
|
+
* would return the following object:
|
|
544
|
+
*
|
|
545
|
+
* ```
|
|
546
|
+
* {
|
|
547
|
+
* id: '1',
|
|
548
|
+
* name: 'apple',
|
|
549
|
+
* color: 'red',
|
|
550
|
+
* taste: 'sweet',
|
|
551
|
+
* }
|
|
552
|
+
* ```
|
|
553
|
+
*
|
|
554
|
+
* @returns Record<string, string>
|
|
555
|
+
*/
|
|
556
|
+
rowsHash() {
|
|
557
|
+
const rows = this.raw();
|
|
558
|
+
const everyRowHasTwoColumns = rows.every((row) => row.length === 2);
|
|
559
|
+
if (!everyRowHasTwoColumns) {
|
|
560
|
+
throw new Error('rowsHash can only be called on a data table where all rows have exactly two columns');
|
|
561
|
+
}
|
|
562
|
+
const result = {};
|
|
563
|
+
rows.forEach((x) => (result[x[0]] = x[1]));
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* This method transposes the DataTable, making the columns into rows
|
|
568
|
+
* and vice versa. For example the following raw table:
|
|
569
|
+
*
|
|
570
|
+
* ```
|
|
571
|
+
* [
|
|
572
|
+
* ['1', 'apple', 'red', 'sweet'],
|
|
573
|
+
* ['2', 'banana', 'yellow', 'sweet'],
|
|
574
|
+
* ['3', 'orange', 'orange', 'sour'],
|
|
575
|
+
* ]
|
|
576
|
+
* ```
|
|
577
|
+
*
|
|
578
|
+
* would be transposed to:
|
|
579
|
+
*
|
|
580
|
+
* ```
|
|
581
|
+
* [
|
|
582
|
+
* ['1', '2', '3'],
|
|
583
|
+
* ['apple', 'banana', 'orange'],
|
|
584
|
+
* ['red', 'yellow', 'orange'],
|
|
585
|
+
* ['sweet', 'sweet', 'sour'],
|
|
586
|
+
* ]
|
|
587
|
+
* ```
|
|
588
|
+
*
|
|
589
|
+
* @returns DataTable
|
|
590
|
+
*/
|
|
591
|
+
transpose() {
|
|
592
|
+
const transposed = this.rawTable[0].map((x, i) => this.rawTable.map((y) => y[i]));
|
|
593
|
+
return new DataTable(transposed);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
436
597
|
class DocString extends String {
|
|
437
598
|
constructor(content, mediaType = '') {
|
|
438
599
|
super(content);
|
|
@@ -488,13 +649,25 @@ const featureRegex = /\.feature(?:\.md)?$/;
|
|
|
488
649
|
const Given = addStepDefinition;
|
|
489
650
|
const When = addStepDefinition;
|
|
490
651
|
const Then = addStepDefinition;
|
|
652
|
+
const stackRegex = /\.feature(?:\.md)?:\d+:\d+/;
|
|
491
653
|
function formatStack(text, line) {
|
|
654
|
+
if (!text.match(stackRegex))
|
|
655
|
+
return text;
|
|
492
656
|
let stack = text.split('\n');
|
|
493
|
-
while (!stack[0].match(
|
|
657
|
+
while (!stack[0].match(stackRegex))
|
|
494
658
|
stack.shift();
|
|
495
659
|
stack[0] = stack[0].replace(/:\d+:\d+$/, `:${line}:1`);
|
|
496
660
|
return stack.join('\n');
|
|
497
661
|
}
|
|
662
|
+
function raceTimeout(work, ms, errorMessage) {
|
|
663
|
+
let timerId;
|
|
664
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
665
|
+
timerId = setTimeout(() => reject(new Error(errorMessage)), ms);
|
|
666
|
+
});
|
|
667
|
+
// make sure to clearTimeout on either success *or* failure
|
|
668
|
+
const wrapped = work.finally(() => clearTimeout(timerId));
|
|
669
|
+
return Promise.race([wrapped, timeoutPromise]);
|
|
670
|
+
}
|
|
498
671
|
const gherkinStep = async (stepType, step, state, line, stepIdx, explodeIdx, data) => {
|
|
499
672
|
try {
|
|
500
673
|
// Set the state info
|
|
@@ -513,27 +686,30 @@ const gherkinStep = async (stepType, step, state, line, stepIdx, explodeIdx, dat
|
|
|
513
686
|
data = new DocString(data.content, data.mediaType);
|
|
514
687
|
dataType = 'docString';
|
|
515
688
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
689
|
+
const promise = async () => {
|
|
690
|
+
await applyHooks('beforeStep', state);
|
|
691
|
+
try {
|
|
692
|
+
const stepDefinitionMatch = findStepDefinitionMatch(step, { stepType, dataType });
|
|
693
|
+
await stepDefinitionMatch.stepDefinition.f(state, ...stepDefinitionMatch.parameters, data);
|
|
694
|
+
}
|
|
695
|
+
catch (e) {
|
|
696
|
+
// Add the Cucumber info to the error message
|
|
697
|
+
e.message = `${step} (#${line})\n${e.message}`;
|
|
698
|
+
// Sort out the stack for the Feature file
|
|
699
|
+
e.stack = formatStack(e.stack, state.info.line);
|
|
700
|
+
// Set the flag that this error has been added to the state
|
|
701
|
+
e.isStepError = true;
|
|
702
|
+
// Add the error to the state
|
|
703
|
+
state.info.errors.push(e);
|
|
704
|
+
// If not in a soft fail mode, re-throw the error
|
|
705
|
+
if (state.isComplete || !state.tagsMatch(state.config.softFailTags))
|
|
706
|
+
throw e;
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
await applyHooks('afterStep', state);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
await raceTimeout(promise(), state.config.stepTimeout, `Step timed out after ${state.config.stepTimeout}ms`);
|
|
537
713
|
}
|
|
538
714
|
catch (e) {
|
|
539
715
|
// If the error hasn't already been added to the state:
|
|
@@ -548,7 +724,7 @@ const gherkinStep = async (stepType, step, state, line, stepIdx, explodeIdx, dat
|
|
|
548
724
|
return;
|
|
549
725
|
// The After hook is usually run in the rendered file, at the end of the rendered steps.
|
|
550
726
|
// But, if the tests have failed, then it should run here, since the test is halted.
|
|
551
|
-
await applyHooks('after', state);
|
|
727
|
+
await raceTimeout(applyHooks('after', state), state.config.stepTimeout, `After hook timed out after ${state.config.stepTimeout}ms`);
|
|
552
728
|
// Otherwise throw the error
|
|
553
729
|
throw e;
|
|
554
730
|
}
|
|
@@ -565,6 +741,10 @@ const defaultConfig = {
|
|
|
565
741
|
* The root directory for the tests to run, from vite or vitest config
|
|
566
742
|
*/
|
|
567
743
|
root: '',
|
|
744
|
+
/**
|
|
745
|
+
* The maximum time in ms to wait for a step to complete.
|
|
746
|
+
*/
|
|
747
|
+
stepTimeout: 3000,
|
|
568
748
|
/**
|
|
569
749
|
* Tags to mark as todo, using Vitest's `test.todo` implementation.
|
|
570
750
|
*/
|
|
@@ -598,7 +778,7 @@ const defaultConfig = {
|
|
|
598
778
|
* Not used by the default World class, but may be used by plugins or custom
|
|
599
779
|
* implementations, like @quickpickle/playwright.
|
|
600
780
|
*/
|
|
601
|
-
worldConfig: {}
|
|
781
|
+
worldConfig: {},
|
|
602
782
|
};
|
|
603
783
|
function is2d(arr) {
|
|
604
784
|
return Array.isArray(arr) && arr.every(item => Array.isArray(item));
|
|
@@ -631,5 +811,5 @@ const quickpickle = (conf = {}) => {
|
|
|
631
811
|
};
|
|
632
812
|
};
|
|
633
813
|
|
|
634
|
-
export { After, AfterAll, AfterStep, Before, BeforeAll, BeforeStep, DocString, Given, QuickPickleWorld, Then, When, applyHooks, quickpickle as default, defaultConfig, defineParameterType, explodeTags, formatStack, getWorldConstructor, gherkinStep, normalizeTags, quickpickle, setWorldConstructor, tagsMatch };
|
|
814
|
+
export { After, AfterAll, AfterStep, Before, BeforeAll, BeforeStep, DataTable, DocString, Given, QuickPickleWorld, Then, When, applyHooks, quickpickle as default, defaultConfig, defineParameterType, explodeTags, formatStack, getWorldConstructor, gherkinStep, normalizeTags, quickpickle, setWorldConstructor, tagsMatch };
|
|
635
815
|
//# sourceMappingURL=index.esm.js.map
|