@letsrunit/gherkin 0.1.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 +44 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +1638 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/src/date.ts +0 -0
- package/src/feature.ts +127 -0
- package/src/index.ts +4 -0
- package/src/keys/parse-key-combo.ts +94 -0
- package/src/locator/compile.ts +172 -0
- package/src/locator/index.ts +2 -0
- package/src/locator/parser.js +1419 -0
- package/src/locator/parser.peggy +101 -0
- package/src/locator/regexp.ts +2 -0
- package/src/parameters.ts +80 -0
- package/src/sanitize.ts +7 -0
- package/src/value.ts +24 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Locator grammar
|
|
2
|
+
// Supports:
|
|
3
|
+
// - role/tag
|
|
4
|
+
// - field / image / text special cases
|
|
5
|
+
// - raw locators in backticks
|
|
6
|
+
// - with / without / within
|
|
7
|
+
//
|
|
8
|
+
// Example:
|
|
9
|
+
// button "Submit" within `form#checkout`
|
|
10
|
+
// section with button "submit" within `#main`
|
|
11
|
+
// section without text "ads" within `css=.foo >> nth(2)`
|
|
12
|
+
|
|
13
|
+
Start
|
|
14
|
+
= _ e:Expr _ { return e; }
|
|
15
|
+
|
|
16
|
+
Expr
|
|
17
|
+
= base:WithExpr rest:(_ "within"i ![a-zA-Z] _ s:Selector { return s; })*
|
|
18
|
+
{ return { type: "Within", base, ancestors: rest ?? [] }; }
|
|
19
|
+
|
|
20
|
+
WithExpr
|
|
21
|
+
= sel:Selector parts:(_ wp:WithPart { return wp; })*
|
|
22
|
+
{
|
|
23
|
+
const include = [], exclude = [];
|
|
24
|
+
for (const x of parts) {
|
|
25
|
+
(x.incl ? include : exclude).push(x.p);
|
|
26
|
+
}
|
|
27
|
+
return { type: "With", selector: sel, include, exclude };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
WithPart
|
|
31
|
+
= "with"i ![a-zA-Z] _ p:Predicate { return { incl: true, p }; }
|
|
32
|
+
/ "without"i ![a-zA-Z] _ p:Predicate { return { incl: false, p }; }
|
|
33
|
+
|
|
34
|
+
// ---------- Selectors ----------
|
|
35
|
+
|
|
36
|
+
Selector
|
|
37
|
+
= RawSel
|
|
38
|
+
/ DateSel
|
|
39
|
+
/ RoleSelFull
|
|
40
|
+
/ TagSelFull
|
|
41
|
+
|
|
42
|
+
// Raw Playwright locator in backticks
|
|
43
|
+
RawSel
|
|
44
|
+
= "`" s:RawChars "`"
|
|
45
|
+
{ return { type: "Selector", mode: "raw", raw: s.trim() }; }
|
|
46
|
+
RawChars
|
|
47
|
+
= $([^`]*)
|
|
48
|
+
|
|
49
|
+
// Role-first: role or special (field, image, text)
|
|
50
|
+
RoleSelFull
|
|
51
|
+
= TheOpt? base:RoleSel
|
|
52
|
+
{ return { type: "Selector", mode: "role", role: base.role, name: base.name }; }
|
|
53
|
+
|
|
54
|
+
DateSel
|
|
55
|
+
= "date"i _ OfOpt? n:DateValue
|
|
56
|
+
{ return { type: "Selector", mode: "date", name: n }; }
|
|
57
|
+
|
|
58
|
+
OfOpt = "of"i ![a-zA-Z] _
|
|
59
|
+
|
|
60
|
+
DateValue
|
|
61
|
+
= s:StringLit { return `"${s}"`; }
|
|
62
|
+
/ $( (!(_ "within"i ![a-zA-Z]) .) + )
|
|
63
|
+
|
|
64
|
+
// Role selection rules with disambiguation:
|
|
65
|
+
// - If the identifier is a known role keyword (button, link, field, image, text), it's a role.
|
|
66
|
+
// - Otherwise, it is treated as a role only when followed by a quoted name.
|
|
67
|
+
RoleSel
|
|
68
|
+
= // Known roles may optionally have a name
|
|
69
|
+
r:KnownRole _ n:StringLit? { return { role: r, name: n ?? null }; }
|
|
70
|
+
/ // Generic role: only considered a role when it has a quoted name
|
|
71
|
+
r:Ident _ n:StringLit { return { role: r, name: n }; }
|
|
72
|
+
|
|
73
|
+
KnownRole
|
|
74
|
+
= "button"i / "link"i / "field"i / "image"i / "text"i / "date"i
|
|
75
|
+
|
|
76
|
+
// Tag-first (e.g. div, section, button)
|
|
77
|
+
TagSelFull
|
|
78
|
+
= TheOpt? t:TagSel
|
|
79
|
+
{ return { type: "Selector", mode: "tag", tag: t }; }
|
|
80
|
+
|
|
81
|
+
TagSel
|
|
82
|
+
= Ident
|
|
83
|
+
|
|
84
|
+
// ---------- Predicates ----------
|
|
85
|
+
|
|
86
|
+
Predicate
|
|
87
|
+
= sel:Selector { return { type: "HasDescendant", selector: sel }; }
|
|
88
|
+
|
|
89
|
+
// ---------- Lexical ----------
|
|
90
|
+
|
|
91
|
+
TheOpt
|
|
92
|
+
= "the"i ![a-zA-Z] _
|
|
93
|
+
|
|
94
|
+
Ident
|
|
95
|
+
= $([a-zA-Z_][a-zA-Z0-9_-]*)
|
|
96
|
+
|
|
97
|
+
StringLit
|
|
98
|
+
= "\"" chars:($(("\\\"" / !("\"") .))* ) "\""
|
|
99
|
+
{ return chars; }
|
|
100
|
+
|
|
101
|
+
_ = [ \t\r\n]*
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Scalar } from '@letsrunit/utils';
|
|
2
|
+
import { KeyCombo, parseKeyCombo } from './keys/parse-key-combo';
|
|
3
|
+
import { compileLocator, locatorRegexp } from './locator';
|
|
4
|
+
import { arrayRegexp, scalarRegexp, valueTransformer } from './value';
|
|
5
|
+
|
|
6
|
+
export interface ParameterTypeDefinition<T> {
|
|
7
|
+
name: string;
|
|
8
|
+
placeholder: string;
|
|
9
|
+
regexp: readonly RegExp[] | readonly string[] | RegExp | string;
|
|
10
|
+
transformer?: (...match: string[]) => T;
|
|
11
|
+
useForSnippets?: boolean;
|
|
12
|
+
preferForRegexpMatch?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function enumToRegexp(values: readonly string[]) {
|
|
16
|
+
const satinized = values.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
17
|
+
return new RegExp(`(${satinized.join('|')})`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function booleanParameter(
|
|
21
|
+
trueValue: string,
|
|
22
|
+
falseValue: string,
|
|
23
|
+
regexp?: RegExp,
|
|
24
|
+
): ParameterTypeDefinition<boolean> {
|
|
25
|
+
return {
|
|
26
|
+
name: trueValue.replace(/\W/, '_'),
|
|
27
|
+
placeholder: `${trueValue}|${falseValue}`,
|
|
28
|
+
regexp: regexp ?? enumToRegexp([trueValue, falseValue]),
|
|
29
|
+
transformer: (value: string): boolean => value === trueValue,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function enumParameter<const T extends readonly string[]>(
|
|
34
|
+
values: T,
|
|
35
|
+
regexp?: RegExp,
|
|
36
|
+
): ParameterTypeDefinition<T[number]> {
|
|
37
|
+
return {
|
|
38
|
+
name: values[0].replace(/\W/, '_'),
|
|
39
|
+
placeholder: values.join('|'),
|
|
40
|
+
regexp: regexp ?? enumToRegexp(values),
|
|
41
|
+
transformer: (value: string): string => value,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function valueParameter(name = 'value'): ParameterTypeDefinition<Scalar | Scalar[]> {
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
placeholder: name,
|
|
49
|
+
regexp: [scalarRegexp, arrayRegexp],
|
|
50
|
+
transformer: valueTransformer,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function locatorParameter(name = 'locator'): ParameterTypeDefinition<string> {
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
placeholder: name,
|
|
58
|
+
regexp: locatorRegexp,
|
|
59
|
+
transformer: (locator: string) => {
|
|
60
|
+
try {
|
|
61
|
+
return compileLocator(locator);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(e);
|
|
64
|
+
return locator;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function keysParameter(name = 'keys'): ParameterTypeDefinition<KeyCombo> {
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
placeholder: name,
|
|
74
|
+
regexp: /"([^"]+)"|'([^']+)'/,
|
|
75
|
+
transformer: (doubleQuoted?: string, singleQuoted?: string): KeyCombo => {
|
|
76
|
+
const raw = (doubleQuoted ?? singleQuoted ?? '').trim();
|
|
77
|
+
return parseKeyCombo(raw);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/sanitize.ts
ADDED
package/src/value.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseDateString, type Scalar } from '@letsrunit/utils';
|
|
2
|
+
|
|
3
|
+
export const scalarRegexp =
|
|
4
|
+
/"((?:[^"\\]+|\\.)*)"|(-?\d+(?:\.\d+)?)|date (?:of )?((?:today|tomorrow|yesterday|\d+ \w+ (?:ago|from now))(?: (?:at )?\d{2}:\d{2}?(?::\d{2})?)?|"\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2})?(?:.\d{3})?Z?)?")/;
|
|
5
|
+
export const arrayRegexp = new RegExp(String.raw`\[(.*?)\]`);
|
|
6
|
+
|
|
7
|
+
function transformScalar(str?: string, num?: string, date?: string): Scalar {
|
|
8
|
+
if (str != null) return str;
|
|
9
|
+
if (num != null) return Number(num);
|
|
10
|
+
if (date) return parseDateString(date);
|
|
11
|
+
|
|
12
|
+
throw new Error('Unexpected value');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const valueTransformer = (
|
|
16
|
+
str?: string,
|
|
17
|
+
num?: string,
|
|
18
|
+
date?: string,
|
|
19
|
+
arr?: string,
|
|
20
|
+
): Scalar | Scalar[] => {
|
|
21
|
+
return arr
|
|
22
|
+
? Array.from(arr.matchAll(new RegExp(scalarRegexp, 'g')), (m) => transformScalar(m[1], m[2], m[3]))
|
|
23
|
+
: transformScalar(str, num, date);
|
|
24
|
+
};
|