quickpickle 1.7.1 → 1.9.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 +2 -1
- package/dist/index.cjs +205 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +204 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/shims/cucumber.d.ts +10 -0
- package/dist/shims/path-sanitizer.d.ts +34 -0
- package/dist/shims/path.d.ts +12 -0
- package/dist/steps.d.ts +1 -1
- package/dist/tags.d.ts +3 -1
- package/dist/world.d.ts +191 -2
- package/package.json +13 -11
package/dist/index.esm.js
CHANGED
|
@@ -3,6 +3,9 @@ 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 pixelmatch from 'pixelmatch';
|
|
7
|
+
import imageSize from '@coderosh/image-size';
|
|
8
|
+
import { Buffer } from 'buffer';
|
|
6
9
|
|
|
7
10
|
const steps = [];
|
|
8
11
|
const parameterTypeRegistry = new ParameterTypeRegistry();
|
|
@@ -80,10 +83,12 @@ function normalizeTags(tags) {
|
|
|
80
83
|
return tags.filter(Boolean).map(tag => tag.startsWith('@') ? tag : `@${tag}`);
|
|
81
84
|
}
|
|
82
85
|
/**
|
|
86
|
+
* Compares two lists of tags and returns the ones that are shared by both,
|
|
87
|
+
* or null if there are no shared tags.
|
|
83
88
|
*
|
|
84
89
|
* @param confTags string[]
|
|
85
90
|
* @param testTags string[]
|
|
86
|
-
* @returns
|
|
91
|
+
* @returns string[]|null
|
|
87
92
|
*/
|
|
88
93
|
function tagsMatch(confTags, testTags) {
|
|
89
94
|
let tags = intersection(confTags.map(t => t.toLowerCase()), testTags.map(t => t.toLowerCase()));
|
|
@@ -610,10 +615,130 @@ class DocString extends String {
|
|
|
610
615
|
}
|
|
611
616
|
}
|
|
612
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Shim for node:path.normalize
|
|
620
|
+
* @param path string
|
|
621
|
+
* @returns string
|
|
622
|
+
*/
|
|
623
|
+
function normalize(path) {
|
|
624
|
+
// Simple path normalization
|
|
625
|
+
const isAbsolute = path.startsWith('/');
|
|
626
|
+
const parts = path.split(/[/\\]+/).filter(Boolean);
|
|
627
|
+
const normalizedParts = [];
|
|
628
|
+
for (const part of parts) {
|
|
629
|
+
if (part === '.')
|
|
630
|
+
continue;
|
|
631
|
+
if (part === '..') {
|
|
632
|
+
normalizedParts.pop();
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
normalizedParts.push(part);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return (isAbsolute ? '/' : '') + normalizedParts.join('/');
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Shim for node:path.join
|
|
642
|
+
* @param paths string[]
|
|
643
|
+
* @returns string
|
|
644
|
+
*/
|
|
645
|
+
function join(...paths) {
|
|
646
|
+
return normalize(paths.join('/'));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const DEFAULT_OPTIONS = {
|
|
650
|
+
decode: [
|
|
651
|
+
{
|
|
652
|
+
regex: /%2e/g,
|
|
653
|
+
replacement: '.'
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
regex: /%2f/g,
|
|
657
|
+
replacement: '/'
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
regex: /%5c/g,
|
|
661
|
+
replacement: '\\'
|
|
662
|
+
}
|
|
663
|
+
],
|
|
664
|
+
parentDirectoryRegEx: /[\/\\]\.\.[\/\\]/g,
|
|
665
|
+
notAllowedRegEx: /:|\$|!|'|"|@|\+|`|\||=/g
|
|
666
|
+
};
|
|
667
|
+
/**
|
|
668
|
+
* Sanitizes a portion of a path to avoid Path Traversal
|
|
669
|
+
*/
|
|
670
|
+
function sanitize(pathstr, options = DEFAULT_OPTIONS) {
|
|
671
|
+
if (!options)
|
|
672
|
+
options = DEFAULT_OPTIONS;
|
|
673
|
+
if (typeof options !== 'object')
|
|
674
|
+
throw new Error('options must be an object');
|
|
675
|
+
if (!Array.isArray(options.decode))
|
|
676
|
+
options.decode = DEFAULT_OPTIONS.decode;
|
|
677
|
+
if (!options.parentDirectoryRegEx)
|
|
678
|
+
options.parentDirectoryRegEx = DEFAULT_OPTIONS.parentDirectoryRegEx;
|
|
679
|
+
if (!options.notAllowedRegEx)
|
|
680
|
+
options.notAllowedRegEx = DEFAULT_OPTIONS.notAllowedRegEx;
|
|
681
|
+
if (typeof pathstr !== 'string') {
|
|
682
|
+
// Stringify the path
|
|
683
|
+
pathstr = `${pathstr}`;
|
|
684
|
+
}
|
|
685
|
+
let sanitizedPath = pathstr;
|
|
686
|
+
// ################################################################################################################
|
|
687
|
+
// Decode
|
|
688
|
+
options.decode.forEach(decode => {
|
|
689
|
+
sanitizedPath = sanitizedPath.replace(decode.regex, decode.replacement);
|
|
690
|
+
});
|
|
691
|
+
// Remove not allowed characters
|
|
692
|
+
sanitizedPath = sanitizedPath.replace(options.notAllowedRegEx, '');
|
|
693
|
+
// Replace backslashes with normal slashes
|
|
694
|
+
sanitizedPath = sanitizedPath.replace(/[\\]/g, '/');
|
|
695
|
+
// Replace /../ with /
|
|
696
|
+
sanitizedPath = sanitizedPath.replace(options.parentDirectoryRegEx, '/');
|
|
697
|
+
// Remove ../ at pos 0 and /.. at end
|
|
698
|
+
sanitizedPath = sanitizedPath.replace(/^\.\.[\/\\]/g, '/');
|
|
699
|
+
sanitizedPath = sanitizedPath.replace(/[\/\\]\.\.$/g, '/');
|
|
700
|
+
// Replace double (back)slashes with a single slash
|
|
701
|
+
sanitizedPath = sanitizedPath.replace(/[\/\\]+/g, '/');
|
|
702
|
+
// Normalize path
|
|
703
|
+
sanitizedPath = normalize(sanitizedPath);
|
|
704
|
+
// Remove / or \ in the end
|
|
705
|
+
while (sanitizedPath.endsWith('/') || sanitizedPath.endsWith('\\')) {
|
|
706
|
+
sanitizedPath = sanitizedPath.slice(0, -1);
|
|
707
|
+
}
|
|
708
|
+
// Remove / or \ in the beginning
|
|
709
|
+
while (sanitizedPath.startsWith('/') || sanitizedPath.startsWith('\\')) {
|
|
710
|
+
sanitizedPath = sanitizedPath.slice(1);
|
|
711
|
+
}
|
|
712
|
+
// Validate path
|
|
713
|
+
sanitizedPath = join('', sanitizedPath);
|
|
714
|
+
// Remove not allowed characters
|
|
715
|
+
sanitizedPath = sanitizedPath.replace(options.notAllowedRegEx, '');
|
|
716
|
+
// Again change all \ to /
|
|
717
|
+
sanitizedPath = sanitizedPath.replace(/[\\]/g, '/');
|
|
718
|
+
// Replace double (back)slashes with a single slash
|
|
719
|
+
sanitizedPath = sanitizedPath.replace(/[\/\\]+/g, '/');
|
|
720
|
+
// Replace /../ with /
|
|
721
|
+
sanitizedPath = sanitizedPath.replace(options.parentDirectoryRegEx, '/');
|
|
722
|
+
// Remove ./ or / at start
|
|
723
|
+
while (sanitizedPath.startsWith('/') || sanitizedPath.startsWith('./') || sanitizedPath.endsWith('/..') || sanitizedPath.endsWith('/../') || sanitizedPath.startsWith('../') || sanitizedPath.startsWith('/../')) {
|
|
724
|
+
sanitizedPath = sanitizedPath.replace(/^\.\//g, ''); // ^./
|
|
725
|
+
sanitizedPath = sanitizedPath.replace(/^\//g, ''); // ^/
|
|
726
|
+
// Remove ../ | /../ at pos 0 and /.. | /../ at end
|
|
727
|
+
sanitizedPath = sanitizedPath.replace(/^[\/\\]\.\.[\/\\]/g, '/');
|
|
728
|
+
sanitizedPath = sanitizedPath.replace(/^\.\.[\/\\]/g, '/');
|
|
729
|
+
sanitizedPath = sanitizedPath.replace(/[\/\\]\.\.$/g, '/');
|
|
730
|
+
sanitizedPath = sanitizedPath.replace(/[\/\\]\.\.\/$/g, '/');
|
|
731
|
+
}
|
|
732
|
+
// Make sure out is not "."
|
|
733
|
+
sanitizedPath = sanitizedPath.trim() === '.' ? '' : sanitizedPath;
|
|
734
|
+
return sanitizedPath.trim();
|
|
735
|
+
}
|
|
736
|
+
|
|
613
737
|
class QuickPickleWorld {
|
|
614
738
|
constructor(context, info) {
|
|
615
739
|
this._projectRoot = '';
|
|
616
740
|
this.data = {};
|
|
741
|
+
this.sanitizePath = sanitize;
|
|
617
742
|
this.context = context;
|
|
618
743
|
this.common = info.common;
|
|
619
744
|
this.info = { ...info, errors: [] };
|
|
@@ -623,10 +748,44 @@ class QuickPickleWorld {
|
|
|
623
748
|
get config() { return this.info.config; }
|
|
624
749
|
get worldConfig() { return this.info.config.worldConfig; }
|
|
625
750
|
get isComplete() { return this.info.stepIdx === this.info.steps.length; }
|
|
626
|
-
|
|
751
|
+
/**
|
|
752
|
+
* Checks the tags of the Scenario against a provided list of tags,
|
|
753
|
+
* and returns the shared tags, with the "@" prefix character.
|
|
754
|
+
*
|
|
755
|
+
* @param tags tags to check
|
|
756
|
+
* @returns string[]|null
|
|
757
|
+
*/
|
|
627
758
|
tagsMatch(tags) {
|
|
628
759
|
return tagsMatch(tags, this.info.tags);
|
|
629
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Given a provided path-like string, returns a full path that:
|
|
763
|
+
*
|
|
764
|
+
* 1. contains no invalid characters.
|
|
765
|
+
* 2. is a subdirectory of the project root.
|
|
766
|
+
*
|
|
767
|
+
* This is intended for security when retrieving and saving files;
|
|
768
|
+
* it does not slugify filenames or check for a file's existence.
|
|
769
|
+
*
|
|
770
|
+
* @param path string the path to sanitize
|
|
771
|
+
* @return string the sanitized path, including the project root
|
|
772
|
+
*/
|
|
773
|
+
fullPath(path) {
|
|
774
|
+
return `${this._projectRoot}/${this.sanitizePath(path)}`;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* A helper function for when you really just need to wait.
|
|
778
|
+
*
|
|
779
|
+
* @deprecated Waiting for arbitrary amounts of time makes your tests flaky! There are
|
|
780
|
+
* usually better ways to wait for something to happen, and this functionality will be
|
|
781
|
+
* removed from the API as soon we're sure nobody will **EVER** want to use it again.
|
|
782
|
+
* (That may be a long time.)
|
|
783
|
+
*
|
|
784
|
+
* @param ms milliseconds to wait
|
|
785
|
+
*/
|
|
786
|
+
async wait(ms) {
|
|
787
|
+
await new Promise(r => setTimeout(r, ms));
|
|
788
|
+
}
|
|
630
789
|
toString() {
|
|
631
790
|
let parts = [
|
|
632
791
|
this.constructor.name,
|
|
@@ -644,6 +803,48 @@ function getWorldConstructor() {
|
|
|
644
803
|
function setWorldConstructor(constructor) {
|
|
645
804
|
worldConstructor = constructor;
|
|
646
805
|
}
|
|
806
|
+
const defaultScreenshotComparisonOptions = {
|
|
807
|
+
maxDiffPercentage: 0,
|
|
808
|
+
threshold: 0.1,
|
|
809
|
+
alpha: 0.6
|
|
810
|
+
};
|
|
811
|
+
class VisualWorld extends QuickPickleWorld {
|
|
812
|
+
constructor(context, info) {
|
|
813
|
+
super(context, info);
|
|
814
|
+
}
|
|
815
|
+
async init() { }
|
|
816
|
+
get screenshotDir() {
|
|
817
|
+
return this.sanitizePath(this.worldConfig.screenshotDir);
|
|
818
|
+
}
|
|
819
|
+
get screenshotFilename() {
|
|
820
|
+
return `${this.toString().replace(/^.+?Feature: /, 'Feature: ').replace(' ' + this.info.step, '')}.png`;
|
|
821
|
+
}
|
|
822
|
+
get screenshotPath() {
|
|
823
|
+
return this.fullPath(`${this.screenshotDir}/${this.screenshotFilename}`);
|
|
824
|
+
}
|
|
825
|
+
getScreenshotPath(name) {
|
|
826
|
+
if (!name)
|
|
827
|
+
return this.screenshotPath;
|
|
828
|
+
let explodedTags = this.info.explodedIdx ? `_(${this.info.tags.join(',')})` : '';
|
|
829
|
+
return this.fullPath(`${this.screenshotDir}/${name}${explodedTags}.png`);
|
|
830
|
+
}
|
|
831
|
+
async screenshotDiff(actual, expected, opts) {
|
|
832
|
+
// Convert Buffer to ArrayBuffer if needed
|
|
833
|
+
const actualBuffer = actual instanceof Buffer ?
|
|
834
|
+
actual.buffer.slice(actual.byteOffset, actual.byteOffset + actual.byteLength) :
|
|
835
|
+
actual;
|
|
836
|
+
const expectedBuffer = expected instanceof Buffer ?
|
|
837
|
+
expected.buffer.slice(expected.byteOffset, expected.byteOffset + expected.byteLength) :
|
|
838
|
+
expected;
|
|
839
|
+
const { width, height } = await imageSize(actualBuffer);
|
|
840
|
+
const diff = Buffer.from([]);
|
|
841
|
+
const mismatchedPixels = pixelmatch(new Uint8Array(actualBuffer), new Uint8Array(expectedBuffer), diff, width, height, opts);
|
|
842
|
+
const totalPixels = width * height;
|
|
843
|
+
const diffPercentage = (mismatchedPixels / totalPixels) * 100;
|
|
844
|
+
const pass = diffPercentage <= opts.maxDiffPercentage;
|
|
845
|
+
return { pass, diff, diffPercentage };
|
|
846
|
+
}
|
|
847
|
+
}
|
|
647
848
|
|
|
648
849
|
const featureRegex = /\.feature(?:\.md)?$/;
|
|
649
850
|
const Given = addStepDefinition;
|
|
@@ -811,5 +1012,5 @@ const quickpickle = (conf = {}) => {
|
|
|
811
1012
|
};
|
|
812
1013
|
};
|
|
813
1014
|
|
|
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 };
|
|
1015
|
+
export { After, AfterAll, AfterStep, Before, BeforeAll, BeforeStep, DataTable, DocString, Given, QuickPickleWorld, Then, VisualWorld, When, applyHooks, quickpickle as default, defaultConfig, defaultScreenshotComparisonOptions, defineParameterType, explodeTags, formatStack, getWorldConstructor, gherkinStep, normalizeTags, quickpickle, setWorldConstructor, tagsMatch };
|
|
815
1016
|
//# sourceMappingURL=index.esm.js.map
|