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/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(/\.feature(?:\.md)?:\d+:\d+/))
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
- await applyHooks('beforeStep', state);
517
- try {
518
- const stepDefinitionMatch = findStepDefinitionMatch(step, { stepType, dataType });
519
- await stepDefinitionMatch.stepDefinition.f(state, ...stepDefinitionMatch.parameters, data);
520
- }
521
- catch (e) {
522
- // Add the Cucumber info to the error message
523
- e.message = `${step} (#${line})\n${e.message}`;
524
- // Sort out the stack for the Feature file
525
- e.stack = formatStack(e.stack, state.info.line);
526
- // Set the flag that this error has been added to the state
527
- e.isStepError = true;
528
- // Add the error to the state
529
- state.info.errors.push(e);
530
- // If not in a soft fail mode, re-throw the error
531
- if (state.isComplete || !state.tagsMatch(state.config.softFailTags))
532
- throw e;
533
- }
534
- finally {
535
- await applyHooks('afterStep', state);
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