@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.
@@ -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,2 @@
1
+ const SELECTOR = /(?:the )?\w+(?: "[^"]*")?|`([^`]+|\\.)*`/;
2
+ export const locatorRegexp = new RegExp(String.raw`((?:${SELECTOR.source})(?: with(?:in|out)? (?:${SELECTOR.source}))*)`);
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ export function sanitizeStepDefinition<T extends string | RegExp>(step: T): T {
2
+ if (typeof step !== 'string') {
3
+ return step;
4
+ }
5
+
6
+ return step.replace(/\{([^|}]+)\|[^}]+}/g, '{$1}') as any;
7
+ }
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
+ };