@interactors/core 1.0.0-rc1.2 → 1.0.0-rc1.3
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/CHANGELOG.md +16 -0
- package/dist/cjs/constructor.js +62 -126
- package/dist/cjs/constructor.js.map +1 -1
- package/dist/cjs/converge.js +3 -5
- package/dist/cjs/converge.js.map +1 -1
- package/dist/cjs/format.js +50 -0
- package/dist/cjs/format.js.map +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/inspector.js +2 -1
- package/dist/cjs/inspector.js.map +1 -1
- package/dist/cjs/interaction.js +59 -35
- package/dist/cjs/interaction.js.map +1 -1
- package/dist/cjs/match.js +17 -7
- package/dist/cjs/match.js.map +1 -1
- package/dist/cjs/resolvers.js +98 -0
- package/dist/cjs/resolvers.js.map +1 -0
- package/dist/cjs/serialize.js +48 -0
- package/dist/cjs/serialize.js.map +1 -0
- package/dist/constructor.d.ts +0 -6
- package/dist/constructor.d.ts.map +1 -1
- package/dist/converge.d.ts +2 -1
- package/dist/converge.d.ts.map +1 -1
- package/dist/esm/constructor.js +59 -117
- package/dist/esm/constructor.js.map +1 -1
- package/dist/esm/converge.js +3 -5
- package/dist/esm/converge.js.map +1 -1
- package/dist/esm/{format-table.js → format.js} +19 -1
- package/dist/esm/format.js.map +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/inspector.js +2 -1
- package/dist/esm/inspector.js.map +1 -1
- package/dist/esm/interaction.js +56 -29
- package/dist/esm/interaction.js.map +1 -1
- package/dist/esm/match.js +17 -7
- package/dist/esm/match.js.map +1 -1
- package/dist/esm/resolvers.js +88 -0
- package/dist/esm/resolvers.js.map +1 -0
- package/dist/esm/serialize.js +43 -0
- package/dist/esm/serialize.js.map +1 -0
- package/dist/format.d.ts +10 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/inspector.d.ts.map +1 -1
- package/dist/interaction.d.ts +38 -10
- package/dist/interaction.d.ts.map +1 -1
- package/dist/match.d.ts +5 -5
- package/dist/match.d.ts.map +1 -1
- package/dist/resolvers.d.ts +9 -0
- package/dist/resolvers.d.ts.map +1 -0
- package/dist/serialize.d.ts +13 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/specification.d.ts +14 -11
- package/dist/specification.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/constructor.ts +65 -133
- package/src/converge.ts +3 -6
- package/src/format.ts +58 -0
- package/src/index.ts +1 -1
- package/src/inspector.ts +2 -1
- package/src/interaction.ts +101 -36
- package/src/match.ts +23 -15
- package/src/resolvers.ts +100 -0
- package/src/serialize.ts +54 -0
- package/src/specification.ts +16 -13
- package/dist/cjs/format-table.js +0 -30
- package/dist/cjs/format-table.js.map +0 -1
- package/dist/esm/format-table.js.map +0 -1
- package/dist/format-table.d.ts +0 -6
- package/dist/format-table.d.ts.map +0 -1
- package/src/format-table.ts +0 -34
package/src/constructor.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
|
|
3
|
-
import { globals } from '@interactors/globals';
|
|
4
3
|
import { converge } from './converge';
|
|
5
4
|
import { MergeObjects } from './merge-objects';
|
|
6
5
|
import {
|
|
@@ -19,98 +18,14 @@ import {
|
|
|
19
18
|
import { FilterSet } from './filter-set';
|
|
20
19
|
import { Locator } from './locator';
|
|
21
20
|
import { MatchFilter, applyFilter } from './match';
|
|
22
|
-
import {
|
|
21
|
+
import { formatDescription } from './format';
|
|
23
22
|
import { FilterNotMatchingError } from './errors';
|
|
24
|
-
import {
|
|
25
|
-
import { Match } from './match';
|
|
26
|
-
import { NoSuchElementError, NotAbsentError, AmbiguousElementError } from './errors';
|
|
23
|
+
import { createInteraction, AssertionInteraction, ActionInteraction } from './interaction';
|
|
27
24
|
import { isMatcher } from './matcher';
|
|
28
25
|
import { matching } from './matchers/matching';
|
|
26
|
+
import { hasMatchMatching, resolveEmpty, resolveFirst, resolveUnique, unsafeSyncResolveParent, unsafeSyncResolveUnique } from './resolvers';
|
|
29
27
|
|
|
30
28
|
const defaultLocator: FilterDefinition<string, Element> = (element) => element.textContent || "";
|
|
31
|
-
const defaultSelector = 'div';
|
|
32
|
-
|
|
33
|
-
export function findElements<E extends Element>(parentElement: Element, interactor: InteractorOptions<any, any, any>): E[] {
|
|
34
|
-
if(typeof(interactor.specification.selector) === 'function') {
|
|
35
|
-
return interactor.specification.selector(parentElement);
|
|
36
|
-
} else if(interactor.specification.selector === ':root') {
|
|
37
|
-
// this is a bit of a hack, because otherwise there isn't a good way of selecting the root element
|
|
38
|
-
return [parentElement.ownerDocument.querySelector(':root') as E];
|
|
39
|
-
} else {
|
|
40
|
-
return Array.from(parentElement.querySelectorAll(interactor.specification.selector || defaultSelector));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function findMatches(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any>[] {
|
|
45
|
-
if (!interactor.name) {
|
|
46
|
-
throw new Error('One of your interactors was created without a name. Please provide a label for your interactor:\n\tHTML.extend(\'my interactor\') || createInteractor(\'my interactor\')');
|
|
47
|
-
}
|
|
48
|
-
return findElements(parentElement, interactor).map((e) => new Match(e, interactor.filter, interactor.locator));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function findMatchesMatching(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any>[] {
|
|
52
|
-
return findMatches(parentElement, interactor).filter((m) => m.matches);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function findMatchesNonEmpty(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any>[] {
|
|
56
|
-
let matches = findMatches(parentElement, interactor);
|
|
57
|
-
let matching = matches.filter((m) => m.matches);
|
|
58
|
-
if(matching.length > 0) {
|
|
59
|
-
return matching;
|
|
60
|
-
} else if(matches.length === 0) {
|
|
61
|
-
throw new NoSuchElementError(`did not find ${description(interactor)}`);
|
|
62
|
-
} else {
|
|
63
|
-
let table = formatTable({
|
|
64
|
-
headers: interactor.locator ? [interactor.name, ...interactor.filter.asTableHeader()] : interactor.filter.asTableHeader(),
|
|
65
|
-
rows: matches.slice().sort((a, b) => b.sortWeight - a.sortWeight).map((m) => m.asTableRow()),
|
|
66
|
-
});
|
|
67
|
-
throw new NoSuchElementError(`did not find ${description(interactor)}, did you mean one of:\n\n${table}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Given a parent element, and an interactor, find exactly one matching element
|
|
72
|
-
// and return it. If no elements match, raise an error. If more than one
|
|
73
|
-
// element matches, raise an error.
|
|
74
|
-
export function resolveUnique(parentElement: Element, interactor: InteractorOptions<any, any, any>): Element {
|
|
75
|
-
let matching = findMatchesNonEmpty(parentElement, interactor);
|
|
76
|
-
|
|
77
|
-
if(matching.length === 1) {
|
|
78
|
-
return matching[0].element;
|
|
79
|
-
} else {
|
|
80
|
-
let alternatives = matching.map((m) => '- ' + m.elementDescription());
|
|
81
|
-
throw new AmbiguousElementError(`${description(interactor)} matches multiple elements:\n\n${alternatives.join('\n')}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Given a parent element, and an interactor, find all matching elements and
|
|
86
|
-
// return them. If no elements match, raise an error.
|
|
87
|
-
export function resolveNonEmpty(parentElement: Element, interactor: InteractorOptions<any, any, any>): Element[] {
|
|
88
|
-
return findMatchesNonEmpty(parentElement, interactor).map(m => m.element);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Given a parent element, and an interactor, check if there are any matching
|
|
92
|
-
// elements, and throw an error if there are. Otherwise return undefined.
|
|
93
|
-
export function resolveEmpty(parentElement: Element, interactor: InteractorOptions<any, any, any>): void {
|
|
94
|
-
let matching = findMatchesMatching(parentElement, interactor);
|
|
95
|
-
|
|
96
|
-
if(matching.length !== 0) {
|
|
97
|
-
let alternatives = matching.map((m) => '- ' + m.elementDescription());
|
|
98
|
-
throw new NotAbsentError(`${description(interactor)} exists but should not:\n\n${alternatives.join('\n')}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function ownDescription(options: InteractorOptions<any, any, any>): string {
|
|
103
|
-
if(options.locator) {
|
|
104
|
-
return `${options.name} ${options.locator.description} ${options.filter.description}`.trim();
|
|
105
|
-
} else {
|
|
106
|
-
return `${options.name} ${options.filter.description}`.trim();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function description(options: InteractorOptions<any, any, any>): string {
|
|
111
|
-
let ancestorsAndSelf: InteractorOptions<any, any, any>[] = options.ancestors.concat(options);
|
|
112
|
-
return ancestorsAndSelf.reverse().map(ownDescription).join(' within ');
|
|
113
|
-
}
|
|
114
29
|
|
|
115
30
|
/**
|
|
116
31
|
* Removes any default values for a filter from the lookup if that filter is present in the
|
|
@@ -131,14 +46,6 @@ function getLookupFilterForAssertion<E extends Element, F extends Filters<E>>(fi
|
|
|
131
46
|
return lookupFilter;
|
|
132
47
|
}
|
|
133
48
|
|
|
134
|
-
export function unsafeSyncResolveParent(options: InteractorOptions<any, any, any>): Element {
|
|
135
|
-
return options.ancestors.reduce(resolveUnique, globals.document.documentElement);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function unsafeSyncResolveUnique<E extends Element>(options: InteractorOptions<E, any, any>): E {
|
|
139
|
-
return resolveUnique(unsafeSyncResolveParent(options), options) as E;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
49
|
export function instantiateInteractor<E extends Element, F extends Filters<E>, A extends Actions<E, Interactor<E, FilterParams<E, F>>>>(
|
|
143
50
|
options: InteractorOptions<E, F, A>,
|
|
144
51
|
resolver: (options: InteractorOptions<E, F, A>) => E
|
|
@@ -147,35 +54,48 @@ export function instantiateInteractor<E extends Element, F extends Filters<E>, A
|
|
|
147
54
|
options,
|
|
148
55
|
|
|
149
56
|
get description(): string {
|
|
150
|
-
return
|
|
57
|
+
return formatDescription(options);
|
|
151
58
|
},
|
|
152
59
|
|
|
153
|
-
perform<T>(fn: (element: E) => T):
|
|
154
|
-
return
|
|
60
|
+
perform<T>(fn: (element: E) => T): ActionInteraction<E, T> {
|
|
61
|
+
return createInteraction('action', {
|
|
62
|
+
name: "perform",
|
|
63
|
+
interactor,
|
|
64
|
+
args: [],
|
|
65
|
+
description: `run perform on ${formatDescription(options)}`,
|
|
66
|
+
run: (interactor) => converge(() => fn(resolver(interactor.options))),
|
|
67
|
+
});
|
|
155
68
|
},
|
|
156
69
|
|
|
157
|
-
assert(fn: (element: E) => void):
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
70
|
+
assert(fn: (element: E) => void): AssertionInteraction<E> {
|
|
71
|
+
return createInteraction('assertion', {
|
|
72
|
+
name: "assert",
|
|
73
|
+
interactor,
|
|
74
|
+
args: [],
|
|
75
|
+
description: `${formatDescription(options)} asserts`,
|
|
76
|
+
run: (interactor) => converge(() => { fn(resolver(interactor.options)) }),
|
|
162
77
|
});
|
|
163
78
|
},
|
|
164
79
|
|
|
165
|
-
has(filters: FilterParams<E, F>):
|
|
80
|
+
has(filters: FilterParams<E, F>): AssertionInteraction<E> {
|
|
166
81
|
return interactor.is(filters);
|
|
167
82
|
},
|
|
168
83
|
|
|
169
|
-
is(filters: FilterParams<E, F>):
|
|
84
|
+
is(filters: FilterParams<E, F>): AssertionInteraction<E> {
|
|
170
85
|
let filter = new FilterSet(options.specification, filters);
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
86
|
+
return createInteraction('assertion', {
|
|
87
|
+
name: "is",
|
|
88
|
+
interactor,
|
|
89
|
+
args: [filters],
|
|
90
|
+
filters,
|
|
91
|
+
description: `${formatDescription(options)} matches filters: ${filter.description}`,
|
|
92
|
+
run: (interactor) => converge(() => {
|
|
93
|
+
let element = resolver({...interactor.options, filter: getLookupFilterForAssertion(interactor.options.filter, filters) });
|
|
174
94
|
let match = new MatchFilter(element, filter);
|
|
175
95
|
if (!match.matches) {
|
|
176
|
-
throw new FilterNotMatchingError(`${
|
|
96
|
+
throw new FilterNotMatchingError(`${formatDescription(options)} does not match filters:\n\n${match.formatAsExpectations()}`);
|
|
177
97
|
}
|
|
178
|
-
})
|
|
98
|
+
}),
|
|
179
99
|
});
|
|
180
100
|
},
|
|
181
101
|
|
|
@@ -186,24 +106,28 @@ export function instantiateInteractor<E extends Element, F extends Filters<E>, A
|
|
|
186
106
|
}, resolver) as unknown as T;
|
|
187
107
|
},
|
|
188
108
|
|
|
189
|
-
exists():
|
|
190
|
-
return
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
109
|
+
exists(): AssertionInteraction<E> & FilterObject<boolean, Element> {
|
|
110
|
+
return createInteraction('assertion', {
|
|
111
|
+
name: 'exists',
|
|
112
|
+
interactor,
|
|
113
|
+
args: [],
|
|
114
|
+
description: `${formatDescription(options)} exists`,
|
|
115
|
+
run: (interactor) => converge(() => {
|
|
116
|
+
resolveFirst(unsafeSyncResolveParent(interactor.options), options);
|
|
117
|
+
}),
|
|
118
|
+
}, (element) => hasMatchMatching(element, options));
|
|
197
119
|
},
|
|
198
120
|
|
|
199
|
-
absent():
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
121
|
+
absent(): AssertionInteraction<E> & FilterObject<boolean, Element> {
|
|
122
|
+
return createInteraction('assertion', {
|
|
123
|
+
name: 'absent',
|
|
124
|
+
interactor,
|
|
125
|
+
args: [],
|
|
126
|
+
description: `${formatDescription(options)} does not exist`,
|
|
127
|
+
run: (interactor) => converge(() => {
|
|
128
|
+
resolveEmpty(unsafeSyncResolveParent(interactor.options), options);
|
|
129
|
+
}),
|
|
130
|
+
}, (element) => !hasMatchMatching(element, options));
|
|
207
131
|
},
|
|
208
132
|
|
|
209
133
|
apply(parentElement: Element): string {
|
|
@@ -220,10 +144,13 @@ export function instantiateInteractor<E extends Element, F extends Filters<E>, A
|
|
|
220
144
|
if (args.length) {
|
|
221
145
|
actionDescription += ` with ` + args.map((a) => JSON.stringify(a)).join(', ');
|
|
222
146
|
}
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
147
|
+
return createInteraction('action', {
|
|
148
|
+
name: actionName,
|
|
149
|
+
interactor,
|
|
150
|
+
args,
|
|
151
|
+
description: `${actionDescription} on ${formatDescription(options)}`,
|
|
152
|
+
run: (interactor) => action(interactor as Interactor<E, FilterParams<E, F>> & FilterMethods<E, F> & ActionMethods<E, A, Interactor<E, FilterParams<E, F>>>, ...args)
|
|
153
|
+
});
|
|
227
154
|
},
|
|
228
155
|
configurable: true,
|
|
229
156
|
writable: true,
|
|
@@ -236,8 +163,12 @@ export function instantiateInteractor<E extends Element, F extends Filters<E>, A
|
|
|
236
163
|
if (!interactor.hasOwnProperty(filterName)) {
|
|
237
164
|
Object.defineProperty(interactor, filterName, {
|
|
238
165
|
value: function() {
|
|
239
|
-
return
|
|
240
|
-
|
|
166
|
+
return createInteraction('assertion', {
|
|
167
|
+
name: filterName,
|
|
168
|
+
interactor,
|
|
169
|
+
args: [],
|
|
170
|
+
description: `${filterName} of ${formatDescription(options)}`,
|
|
171
|
+
run: (interactor) => converge(() => applyFilter(filter, resolver(interactor.options))),
|
|
241
172
|
}, (parentElement) => {
|
|
242
173
|
let element = [...options.ancestors, options].reduce(resolveUnique, parentElement);
|
|
243
174
|
return applyFilter(filter, element);
|
|
@@ -270,6 +201,7 @@ export function createConstructor<E extends Element, FP extends FilterParams<any
|
|
|
270
201
|
}
|
|
271
202
|
|
|
272
203
|
return Object.assign(initInteractor, {
|
|
204
|
+
interactorName: name,
|
|
273
205
|
selector: (value: string): InteractorConstructor<E, FP, FM, AM> => {
|
|
274
206
|
return createConstructor(name, { ...specification, selector: value });
|
|
275
207
|
},
|
package/src/converge.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
+
import { Operation, sleep } from '@effection/core';
|
|
1
2
|
import { performance } from 'performance-api';
|
|
2
3
|
import { globals } from '@interactors/globals'
|
|
3
4
|
|
|
4
|
-
function
|
|
5
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function converge<T>(fn: () => T): Promise<T> {
|
|
5
|
+
export function* converge<T>(fn: () => T): Operation<T> {
|
|
9
6
|
let startTime = performance.now();
|
|
10
7
|
while(true) {
|
|
11
8
|
try {
|
|
@@ -15,7 +12,7 @@ export async function converge<T>(fn: () => T): Promise<T> {
|
|
|
15
12
|
if(diff > globals.interactorTimeout) {
|
|
16
13
|
throw e;
|
|
17
14
|
} else {
|
|
18
|
-
|
|
15
|
+
yield sleep(1);
|
|
19
16
|
}
|
|
20
17
|
}
|
|
21
18
|
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { Match } from "./match";
|
|
3
|
+
import { InteractorOptions } from "./specification";
|
|
4
|
+
|
|
5
|
+
export interface TableOptions {
|
|
6
|
+
headers: string[];
|
|
7
|
+
rows: string[][];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MAX_COLUMN_WIDTH = 40
|
|
11
|
+
|
|
12
|
+
function formatValue(value: string, width: number) {
|
|
13
|
+
if(value.length > width) {
|
|
14
|
+
return value.slice(0, width - 1) + '…';
|
|
15
|
+
} else {
|
|
16
|
+
return value.padEnd(width);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatTable(options: TableOptions): string {
|
|
21
|
+
let columnWidths = options.headers.map((h, index) => {
|
|
22
|
+
return Math.min(MAX_COLUMN_WIDTH, Math.max(h.length, ...options.rows.map((r) => r[index].length)));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let formatRow = (cells: string[]) => {
|
|
26
|
+
return '┃ ' + cells.map((c, index) => formatValue(c, columnWidths[index])).join(' ┃ ') + ' ┃';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let spacerRow = () => {
|
|
30
|
+
return '┣━' + columnWidths.map((w) => "━".repeat(w)).join('━╋━') + '━┫';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
formatRow(options.headers),
|
|
35
|
+
spacerRow(),
|
|
36
|
+
...options.rows.map((row) => formatRow(row))
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ownDescription(options: InteractorOptions<any, any, any>): string {
|
|
41
|
+
if(options.locator) {
|
|
42
|
+
return `${options.name} ${options.locator.description} ${options.filter.description}`.trim();
|
|
43
|
+
} else {
|
|
44
|
+
return `${options.name} ${options.filter.description}`.trim();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatDescription(options: InteractorOptions<any, any, any>): string {
|
|
49
|
+
let ancestorsAndSelf: InteractorOptions<any, any, any>[] = options.ancestors.concat(options);
|
|
50
|
+
return ancestorsAndSelf.reverse().map(ownDescription).join(' within ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatMatchesTable(interactor: InteractorOptions<any, any, any>, matches: Match<Element, any>[]): string {
|
|
54
|
+
return formatTable({
|
|
55
|
+
headers: interactor.locator ? [interactor.name, ...interactor.filter.asTableHeader()] : interactor.filter.asTableHeader(),
|
|
56
|
+
rows: matches.slice().sort((a, b) => b.sortWeight - a.sortWeight).map((m) => m.asTableRow()),
|
|
57
|
+
})
|
|
58
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Interactor, InteractorConstructor, InteractorSpecification, EmptyObject, FilterMethods, ActionMethods } from './specification';
|
|
2
|
-
export { Interaction,
|
|
2
|
+
export { Interaction, ActionInteraction, AssertionInteraction, isInteraction } from './interaction';
|
|
3
3
|
export { createInteractor } from './create-interactor';
|
|
4
4
|
export { createInspector } from './inspector'
|
|
5
5
|
export { isVisible } from 'element-is-visible';
|
package/src/inspector.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { instantiateInteractor } from "./constructor";
|
|
4
|
+
import { findElements, unsafeSyncResolveParent } from './resolvers'
|
|
4
5
|
import { Interactor, Filters, InteractorConstructor, FilterParams } from "./specification";
|
|
5
6
|
|
|
6
7
|
type GetElement<I extends InteractorConstructor<any, any, any, any>> = I extends InteractorConstructor<infer E, any, any, any> ? E : never
|
package/src/interaction.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { Operation, Task, run, Symbol } from '@effection/core';
|
|
3
|
+
import { InteractionOptions as SerializedInteractionOptions, globals, InteractionType } from '@interactors/globals';
|
|
4
|
+
import type { Interactor, FilterObject, FilterFn, FilterParams } from './specification';
|
|
5
|
+
import { serializeInteractionOptions } from './serialize';
|
|
3
6
|
|
|
4
|
-
const interactionSymbol = Symbol.for('interaction');
|
|
7
|
+
const interactionSymbol: unique symbol = Symbol.for('interaction') as any;
|
|
5
8
|
|
|
6
|
-
export function isInteraction(x: unknown): x is Interaction<unknown> {
|
|
9
|
+
export function isInteraction(x: unknown): x is Interaction<Element, unknown> {
|
|
7
10
|
return typeof x === 'object' && x != null && interactionSymbol in x
|
|
8
11
|
}
|
|
9
12
|
|
|
@@ -18,17 +21,38 @@ export function isInteraction(x: unknown): x is Interaction<unknown> {
|
|
|
18
21
|
*
|
|
19
22
|
* @typeParam T the return value of the promise that this interaction evaluates to.
|
|
20
23
|
*/
|
|
21
|
-
export interface Interaction<T> extends Promise<T> {
|
|
24
|
+
export interface Interaction<E extends Element, T = void> extends Promise<T> {
|
|
25
|
+
type: InteractionType;
|
|
26
|
+
|
|
27
|
+
interactor: Interactor<E, any>;
|
|
28
|
+
run: (interactor: Interactor<E, any>) => Operation<T>;
|
|
22
29
|
/**
|
|
23
30
|
* Return a description of the interaction
|
|
24
31
|
*/
|
|
25
32
|
description: string;
|
|
33
|
+
/**
|
|
34
|
+
* Return a code representation of the interaction
|
|
35
|
+
*/
|
|
36
|
+
code: () => string;
|
|
37
|
+
/**
|
|
38
|
+
* Return a serialized options of the interaction
|
|
39
|
+
*/
|
|
40
|
+
options: SerializedInteractionOptions;
|
|
26
41
|
/**
|
|
27
42
|
* Perform the interaction
|
|
28
43
|
*/
|
|
29
|
-
action: () =>
|
|
44
|
+
action: () => Task<T>;
|
|
45
|
+
|
|
46
|
+
check?: () => Task<T>;
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
halt: () => Promise<void>;
|
|
49
|
+
|
|
50
|
+
[interactionSymbol]: true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ActionInteraction<E extends Element, T = void> extends Interaction<E, T> {
|
|
54
|
+
type: "action";
|
|
55
|
+
check: undefined;
|
|
32
56
|
}
|
|
33
57
|
|
|
34
58
|
/**
|
|
@@ -36,47 +60,88 @@ export interface Interaction<T> extends Promise<T> {
|
|
|
36
60
|
*
|
|
37
61
|
* @typeParam T the return value of the promise that this interaction evaluates to.
|
|
38
62
|
*/
|
|
39
|
-
export interface
|
|
63
|
+
export interface AssertionInteraction<E extends Element, T = void> extends Interaction<E, T> {
|
|
64
|
+
type: "assertion";
|
|
40
65
|
/**
|
|
41
66
|
* Perform the check
|
|
42
67
|
*/
|
|
43
|
-
check: () =>
|
|
68
|
+
check: () => Task<T>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type InteractionOptions<E extends Element, T> = {
|
|
72
|
+
name: string;
|
|
73
|
+
description: string;
|
|
74
|
+
filters?: FilterParams<any, any>;
|
|
75
|
+
args: unknown[];
|
|
76
|
+
interactor: Interactor<E, any>;
|
|
77
|
+
run: (interactor: Interactor<E, any>) => Operation<T>;
|
|
44
78
|
}
|
|
45
79
|
|
|
46
|
-
function createInteraction<T>(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
export function createInteraction<E extends Element, T>(type: 'action', options: InteractionOptions<E, T>): ActionInteraction<E, T>
|
|
81
|
+
export function createInteraction<E extends Element, T>(type: 'assertion', options: InteractionOptions<E, T>): AssertionInteraction<E, T>
|
|
82
|
+
export function createInteraction<E extends Element, T, Q>(type: 'action', options: InteractionOptions<E, T>, apply: FilterFn<Q, Element>): ActionInteraction<E, T> & FilterObject<Q, Element>
|
|
83
|
+
export function createInteraction<E extends Element, T, Q>(type: 'assertion', options: InteractionOptions<E, T>, apply: FilterFn<Q, Element>): AssertionInteraction<E, T> & FilterObject<Q, Element>
|
|
84
|
+
export function createInteraction<E extends Element, T, Q>(type: InteractionType, options: InteractionOptions<E, T>, apply?: FilterFn<Q, Element>): Interaction<E, T> & Operation<T> {
|
|
85
|
+
let task: Task<T>;
|
|
86
|
+
let shouldCatchHalt = false
|
|
87
|
+
|
|
88
|
+
function operation(scope: Task): Operation<T> {
|
|
89
|
+
let run = () => options.run(options.interactor);
|
|
90
|
+
|
|
91
|
+
return (globals.wrapInteraction
|
|
92
|
+
? globals.wrapInteraction(() => scope.run(run), interaction)
|
|
93
|
+
: globals.wrapAction(interaction.description, () => scope.run(run), type)) as Operation<T>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function action(): Task<T> {
|
|
97
|
+
if(!task) {
|
|
98
|
+
task = run(operation);
|
|
99
|
+
}
|
|
100
|
+
return task;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function catchHalt<T extends (reason: any) => any>(onReject?: T | null) {
|
|
104
|
+
return (reason: any) => {
|
|
105
|
+
if (shouldCatchHalt && reason instanceof Error && reason.message == 'halted') {
|
|
106
|
+
return reason
|
|
107
|
+
} else {
|
|
108
|
+
onReject?.(reason)
|
|
109
|
+
return reason
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let serializedOptions = serializeInteractionOptions(type, options)
|
|
115
|
+
let interaction: Interaction<E, T> & Operation<T> = {
|
|
116
|
+
type,
|
|
117
|
+
options: serializedOptions,
|
|
118
|
+
description: options.description,
|
|
119
|
+
interactor: options.interactor,
|
|
120
|
+
run: options.run,
|
|
121
|
+
args: options.args,
|
|
50
122
|
action,
|
|
123
|
+
check: type === 'assertion' ? action : undefined,
|
|
124
|
+
code: () => serializedOptions.code(),
|
|
125
|
+
halt: () => {
|
|
126
|
+
shouldCatchHalt = true
|
|
127
|
+
return action().halt()
|
|
128
|
+
},
|
|
51
129
|
[interactionSymbol]: true,
|
|
52
|
-
[Symbol.toStringTag]: `[interaction ${description}]`,
|
|
130
|
+
[Symbol.toStringTag]: `[interaction ${options.description}]`,
|
|
131
|
+
[Symbol.operation]: operation,
|
|
53
132
|
then(onFulfill, onReject) {
|
|
54
|
-
|
|
55
|
-
return promise.then(onFulfill, onReject);
|
|
133
|
+
return action().then(onFulfill, catchHalt(onReject));
|
|
56
134
|
},
|
|
57
135
|
catch(onReject) {
|
|
58
|
-
|
|
59
|
-
return promise.catch(onReject);
|
|
136
|
+
return action().catch(catchHalt(onReject));
|
|
60
137
|
},
|
|
61
138
|
finally(handler) {
|
|
62
|
-
|
|
63
|
-
return promise.finally(handler);
|
|
139
|
+
return action().finally(handler);
|
|
64
140
|
}
|
|
65
141
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function check<T>(description: string, check: () => Promise<T>): ReadonlyInteraction<T> {
|
|
73
|
-
return { check() { return this.action() }, ...createInteraction(description, globals.wrapAction(description, check, 'check')) };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function interactionFilter<T, Q>(description: string, action: () => Promise<T>, filter: (element: Element) => Q): Interaction<T> & FilterObject<Q, Element> {
|
|
77
|
-
return { apply: filter, ...interaction(description, action) };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function checkFilter<T, Q>(description: string, action: () => Promise<T>, filter: (element: Element) => Q): ReadonlyInteraction<T> & FilterObject<Q, Element> {
|
|
81
|
-
return { apply: filter , ...check(description, action) };
|
|
142
|
+
if (apply) {
|
|
143
|
+
return Object.assign(interaction, { apply });
|
|
144
|
+
} else {
|
|
145
|
+
return interaction;
|
|
146
|
+
}
|
|
82
147
|
}
|
package/src/match.ts
CHANGED
|
@@ -9,7 +9,6 @@ const check = (value: unknown): string => value ? "✓" : "⨯";
|
|
|
9
9
|
export class Match<E extends Element, F extends Filters<E>> {
|
|
10
10
|
public matchLocator?: MatchLocator<E>;
|
|
11
11
|
public matchFilter: MatchFilter<E, F>;
|
|
12
|
-
public matches: boolean;
|
|
13
12
|
|
|
14
13
|
constructor(
|
|
15
14
|
public element: E,
|
|
@@ -18,7 +17,10 @@ export class Match<E extends Element, F extends Filters<E>> {
|
|
|
18
17
|
) {
|
|
19
18
|
this.matchLocator = locator && new MatchLocator(element, locator);
|
|
20
19
|
this.matchFilter = new MatchFilter(element, filterSet);
|
|
21
|
-
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get matches(): boolean {
|
|
23
|
+
return (this.matchLocator ? this.matchLocator.matches : true) && this.matchFilter.matches;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
asTableHeader(name: string): string[] {
|
|
@@ -51,7 +53,6 @@ export class Match<E extends Element, F extends Filters<E>> {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
export class MatchLocator<E extends Element> {
|
|
54
|
-
public matches: boolean;
|
|
55
56
|
public expected: MaybeMatcher<string> | null;
|
|
56
57
|
public actual: string | null;
|
|
57
58
|
|
|
@@ -61,7 +62,10 @@ export class MatchLocator<E extends Element> {
|
|
|
61
62
|
) {
|
|
62
63
|
this.expected = locator.value;
|
|
63
64
|
this.actual = locator.apply(element);
|
|
64
|
-
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get matches(): boolean {
|
|
68
|
+
return applyMatcher(this.expected, this.actual)
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
formatActual(): string {
|
|
@@ -78,7 +82,6 @@ export class MatchLocator<E extends Element> {
|
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
export class MatchFilter<E extends Element, F extends Filters<E>> {
|
|
81
|
-
public matches: boolean;
|
|
82
85
|
public items: MatchFilterItem<unknown, E, F>[];
|
|
83
86
|
|
|
84
87
|
constructor(
|
|
@@ -88,7 +91,10 @@ export class MatchFilter<E extends Element, F extends Filters<E>> {
|
|
|
88
91
|
this.items = Object.entries(filter.all).map(([key, expected]) => {
|
|
89
92
|
return new MatchFilterItem(element, filter, key, expected)
|
|
90
93
|
});
|
|
91
|
-
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get matches(): boolean {
|
|
97
|
+
return this.items.every((match) => match.matches)
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
asTableRow(): string[] {
|
|
@@ -114,23 +120,25 @@ export function applyFilter<T>(definition: FilterDefinition<T, any>, element: El
|
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
export class MatchFilterItem<T, E extends Element, F extends Filters<E>> {
|
|
117
|
-
public actual: T;
|
|
118
|
-
public matches: boolean;
|
|
119
|
-
|
|
120
123
|
constructor(
|
|
121
124
|
public element: E,
|
|
122
125
|
public filter: FilterSet<E, F>,
|
|
123
126
|
public key: string,
|
|
124
127
|
public expected: MaybeMatcher<T>,
|
|
125
|
-
) {
|
|
126
|
-
|
|
128
|
+
) {}
|
|
129
|
+
|
|
130
|
+
get matches(): boolean {
|
|
131
|
+
return applyMatcher(this.expected, this.actual);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get actual(): T {
|
|
135
|
+
if (this.filter.specification.filters && this.filter.specification.filters[this.key]) {
|
|
127
136
|
let definition = this.filter.specification.filters[this.key];
|
|
128
|
-
if(typeof
|
|
129
|
-
|
|
137
|
+
if (typeof definition === 'function') {
|
|
138
|
+
return definition(this.element) as T;
|
|
130
139
|
} else {
|
|
131
|
-
|
|
140
|
+
return definition.apply(this.element) as T;
|
|
132
141
|
}
|
|
133
|
-
this.matches = applyMatcher(this.expected, this.actual);
|
|
134
142
|
} else {
|
|
135
143
|
throw new Error(`interactor does not define a filter named ${JSON.stringify(this.key)}`);
|
|
136
144
|
}
|