@interactors/core 1.0.0-rc1.1 → 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 +22 -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 +2 -8
- 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 +4 -2
- 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 +20 -17
- package/dist/specification.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/constructor.ts +70 -138
- package/src/converge.ts +3 -6
- package/src/format.ts +58 -0
- package/src/index.ts +4 -2
- 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 +22 -19
- 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,51 +46,56 @@ function getLookupFilterForAssertion<E extends Element, F extends Filters<E>>(fi
|
|
|
131
46
|
return lookupFilter;
|
|
132
47
|
}
|
|
133
48
|
|
|
134
|
-
export function
|
|
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
|
-
export function instantiateInteractor<E extends Element, F extends Filters<E>, A extends Actions<E>>(
|
|
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
|
|
145
|
-
): Interactor<E, FilterParams<E, F>> & ActionMethods<E, A
|
|
52
|
+
): Interactor<E, FilterParams<E, F>> & FilterMethods<E, F> & ActionMethods<E, A, Interactor<E, FilterParams<E, F>>> {
|
|
146
53
|
let interactor = {
|
|
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);
|
|
@@ -250,10 +181,10 @@ export function instantiateInteractor<E extends Element, F extends Filters<E>, A
|
|
|
250
181
|
}
|
|
251
182
|
}
|
|
252
183
|
|
|
253
|
-
return interactor as Interactor<E, FilterParams<E, F>> & ActionMethods<E, A
|
|
184
|
+
return interactor as Interactor<E, FilterParams<E, F>> & FilterMethods<E, F> & ActionMethods<E, A, Interactor<E, FilterParams<E, F>>>;
|
|
254
185
|
}
|
|
255
186
|
|
|
256
|
-
export function createConstructor<E extends Element, FP extends FilterParams<any, any>, FM extends FilterMethods<any, any>, AM extends ActionMethods<any, any>>(
|
|
187
|
+
export function createConstructor<E extends Element, FP extends FilterParams<any, any>, FM extends FilterMethods<any, any>, AM extends ActionMethods<any, any, any>>(
|
|
257
188
|
name: string,
|
|
258
189
|
specification: InteractorSpecification<E, any, any>,
|
|
259
190
|
): InteractorConstructor<E, FP, FM, AM> {
|
|
@@ -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
|
},
|
|
@@ -279,7 +211,7 @@ export function createConstructor<E extends Element, FP extends FilterParams<any
|
|
|
279
211
|
filters: <FR extends Filters<E>>(filters: FR): InteractorConstructor<E, MergeObjects<FP, FilterParams<E, FR>>, MergeObjects<FM, FilterMethods<E, FR>>, AM> => {
|
|
280
212
|
return createConstructor(name, { ...specification, filters: { ...specification.filters, ...filters } });
|
|
281
213
|
},
|
|
282
|
-
actions: <AR extends Actions<E>>(actions: AR): InteractorConstructor<E, FP, FM, MergeObjects<AM, ActionMethods<E, AR>>> => {
|
|
214
|
+
actions: <I extends Interactor<E, FP> & FM & AM, AR extends Actions<E, I>>(actions: AR): InteractorConstructor<E, FP, FM, MergeObjects<AM, ActionMethods<E, AR, I>>> => {
|
|
283
215
|
return createConstructor(name, { ...specification, actions: Object.assign({}, specification.actions, actions) });
|
|
284
216
|
},
|
|
285
217
|
extend: <ER extends Element = E>(newName: string): InteractorConstructor<ER, FP, FM, AM> => {
|
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
|
-
export { Interactor, InteractorConstructor, InteractorSpecification } from './specification';
|
|
2
|
-
export { Interaction,
|
|
1
|
+
export { Interactor, InteractorConstructor, InteractorSpecification, EmptyObject, FilterMethods, ActionMethods } from './specification';
|
|
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';
|
|
@@ -14,3 +14,5 @@ export { some } from './matchers/some';
|
|
|
14
14
|
export { every } from './matchers/every';
|
|
15
15
|
|
|
16
16
|
export { click } from './element/click';
|
|
17
|
+
export { MergeObjects } from './merge-objects'
|
|
18
|
+
export { MaybeMatcher } from './matcher'
|
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
|
}
|