@interactors/core 0.3.0 → 1.0.0-effection.1
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/dist/cjs/constructor.js +66 -143
- 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 +1 -20
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/inspector.js +3 -2
- package/dist/cjs/inspector.js.map +1 -1
- package/dist/cjs/interaction.js +43 -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/matcher.js +13 -1
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/matchers/and.js +3 -0
- package/dist/cjs/matchers/and.js.map +1 -1
- package/dist/cjs/matchers/every.js +3 -0
- package/dist/cjs/matchers/every.js.map +1 -1
- package/dist/cjs/matchers/including.js +4 -0
- package/dist/cjs/matchers/including.js.map +1 -1
- package/dist/cjs/matchers/matching.js +4 -0
- package/dist/cjs/matchers/matching.js.map +1 -1
- package/dist/cjs/matchers/not.js +3 -0
- package/dist/cjs/matchers/not.js.map +1 -1
- package/dist/cjs/matchers/or.js +3 -0
- package/dist/cjs/matchers/or.js.map +1 -1
- package/dist/cjs/matchers/some.js +3 -0
- package/dist/cjs/matchers/some.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 +3 -10
- 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 +64 -134
- 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 +0 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/inspector.js +3 -2
- package/dist/esm/inspector.js.map +1 -1
- package/dist/esm/interaction.js +40 -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/matcher.js +11 -0
- package/dist/esm/matcher.js.map +1 -1
- package/dist/esm/matchers/and.js +4 -1
- package/dist/esm/matchers/and.js.map +1 -1
- package/dist/esm/matchers/every.js +4 -1
- package/dist/esm/matchers/every.js.map +1 -1
- package/dist/esm/matchers/including.js +4 -0
- package/dist/esm/matchers/including.js.map +1 -1
- package/dist/esm/matchers/matching.js +4 -0
- package/dist/esm/matchers/matching.js.map +1 -1
- package/dist/esm/matchers/not.js +4 -1
- package/dist/esm/matchers/not.js.map +1 -1
- package/dist/esm/matchers/or.js +4 -1
- package/dist/esm/matchers/or.js.map +1 -1
- package/dist/esm/matchers/some.js +4 -1
- package/dist/esm/matchers/some.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 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/inspector.d.ts +2 -2
- 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/matcher.d.ts +2 -0
- package/dist/matcher.d.ts.map +1 -1
- package/dist/matchers/and.d.ts.map +1 -1
- package/dist/matchers/every.d.ts.map +1 -1
- package/dist/matchers/including.d.ts.map +1 -1
- package/dist/matchers/matching.d.ts.map +1 -1
- package/dist/matchers/not.d.ts.map +1 -1
- package/dist/matchers/or.d.ts.map +1 -1
- package/dist/matchers/some.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 +49 -51
- package/dist/specification.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/constructor.ts +76 -160
- package/src/converge.ts +3 -6
- package/src/format.ts +58 -0
- package/src/index.ts +4 -6
- package/src/inspector.ts +5 -4
- package/src/interaction.ts +85 -36
- package/src/match.ts +23 -15
- package/src/matcher.ts +11 -0
- package/src/matchers/and.ts +4 -1
- package/src/matchers/every.ts +4 -1
- package/src/matchers/including.ts +4 -1
- package/src/matchers/matching.ts +4 -1
- package/src/matchers/not.ts +4 -1
- package/src/matchers/or.ts +4 -1
- package/src/matchers/some.ts +4 -1
- package/src/resolvers.ts +100 -0
- package/src/serialize.ts +54 -0
- package/src/specification.ts +58 -61
- package/CHANGELOG.md +0 -14
- package/dist/cjs/dispatch.js +0 -34
- package/dist/cjs/dispatch.js.map +0 -1
- package/dist/cjs/element/fill-in.js +0 -71
- package/dist/cjs/element/fill-in.js.map +0 -1
- package/dist/cjs/focused.js +0 -47
- package/dist/cjs/focused.js.map +0 -1
- package/dist/cjs/format-table.js +0 -30
- package/dist/cjs/format-table.js.map +0 -1
- package/dist/cjs/perform.js +0 -13
- package/dist/cjs/perform.js.map +0 -1
- package/dist/dispatch.d.ts +0 -6
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/element/fill-in.d.ts +0 -12
- package/dist/element/fill-in.d.ts.map +0 -1
- package/dist/esm/dispatch.js +0 -26
- package/dist/esm/dispatch.js.map +0 -1
- package/dist/esm/element/fill-in.js +0 -67
- package/dist/esm/element/fill-in.js.map +0 -1
- package/dist/esm/focused.js +0 -41
- package/dist/esm/focused.js.map +0 -1
- package/dist/esm/format-table.js.map +0 -1
- package/dist/esm/perform.js +0 -9
- package/dist/esm/perform.js.map +0 -1
- package/dist/focused.d.ts +0 -30
- package/dist/focused.d.ts.map +0 -1
- package/dist/format-table.d.ts +0 -6
- package/dist/format-table.d.ts.map +0 -1
- package/dist/perform.d.ts +0 -3
- package/dist/perform.d.ts.map +0 -1
- package/src/dispatch.ts +0 -24
- package/src/element/fill-in.ts +0 -76
- package/src/focused.ts +0 -43
- package/src/format-table.ts +0 -34
- package/src/perform.ts +0 -11
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 {
|
|
@@ -14,104 +13,19 @@ import {
|
|
|
14
13
|
FilterParams,
|
|
15
14
|
FilterMethods,
|
|
16
15
|
InteractorSpecification,
|
|
17
|
-
BaseInteractor,
|
|
18
16
|
FilterObject,
|
|
19
17
|
} from './specification';
|
|
20
18
|
import { FilterSet } from './filter-set';
|
|
21
19
|
import { Locator } from './locator';
|
|
22
20
|
import { MatchFilter, applyFilter } from './match';
|
|
23
|
-
import {
|
|
21
|
+
import { formatDescription } from './format';
|
|
24
22
|
import { FilterNotMatchingError } from './errors';
|
|
25
|
-
import {
|
|
26
|
-
import { Match } from './match';
|
|
27
|
-
import { NoSuchElementError, NotAbsentError, AmbiguousElementError } from './errors';
|
|
23
|
+
import { createInteraction, AssertionInteraction, ActionInteraction } from './interaction';
|
|
28
24
|
import { isMatcher } from './matcher';
|
|
29
25
|
import { matching } from './matchers/matching';
|
|
26
|
+
import { hasMatchMatching, resolveEmpty, resolveFirst, resolveUnique, unsafeSyncResolveParent, unsafeSyncResolveUnique } from './resolvers';
|
|
30
27
|
|
|
31
28
|
const defaultLocator: FilterDefinition<string, Element> = (element) => element.textContent || "";
|
|
32
|
-
const defaultSelector = 'div';
|
|
33
|
-
|
|
34
|
-
export function findElements<E extends Element>(parentElement: Element, interactor: InteractorOptions<any, any, any>): E[] {
|
|
35
|
-
if(typeof(interactor.specification.selector) === 'function') {
|
|
36
|
-
return interactor.specification.selector(parentElement);
|
|
37
|
-
} else if(interactor.specification.selector === ':root') {
|
|
38
|
-
// this is a bit of a hack, because otherwise there isn't a good way of selecting the root element
|
|
39
|
-
return [parentElement.ownerDocument.querySelector(':root') as E];
|
|
40
|
-
} else {
|
|
41
|
-
return Array.from(parentElement.querySelectorAll(interactor.specification.selector || defaultSelector));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function findMatches(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any>[] {
|
|
46
|
-
if (!interactor.name) {
|
|
47
|
-
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\')');
|
|
48
|
-
}
|
|
49
|
-
return findElements(parentElement, interactor).map((e) => new Match(e, interactor.filter, interactor.locator));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function findMatchesMatching(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any>[] {
|
|
53
|
-
return findMatches(parentElement, interactor).filter((m) => m.matches);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function findMatchesNonEmpty(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any>[] {
|
|
57
|
-
let matches = findMatches(parentElement, interactor);
|
|
58
|
-
let matching = matches.filter((m) => m.matches);
|
|
59
|
-
if(matching.length > 0) {
|
|
60
|
-
return matching;
|
|
61
|
-
} else if(matches.length === 0) {
|
|
62
|
-
throw new NoSuchElementError(`did not find ${description(interactor)}`);
|
|
63
|
-
} else {
|
|
64
|
-
let table = formatTable({
|
|
65
|
-
headers: interactor.locator ? [interactor.name, ...interactor.filter.asTableHeader()] : interactor.filter.asTableHeader(),
|
|
66
|
-
rows: matches.slice().sort((a, b) => b.sortWeight - a.sortWeight).map((m) => m.asTableRow()),
|
|
67
|
-
});
|
|
68
|
-
throw new NoSuchElementError(`did not find ${description(interactor)}, did you mean one of:\n\n${table}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Given a parent element, and an interactor, find exactly one matching element
|
|
73
|
-
// and return it. If no elements match, raise an error. If more than one
|
|
74
|
-
// element matches, raise an error.
|
|
75
|
-
export function resolveUnique(parentElement: Element, interactor: InteractorOptions<any, any, any>): Element {
|
|
76
|
-
let matching = findMatchesNonEmpty(parentElement, interactor);
|
|
77
|
-
|
|
78
|
-
if(matching.length === 1) {
|
|
79
|
-
return matching[0].element;
|
|
80
|
-
} else {
|
|
81
|
-
let alternatives = matching.map((m) => '- ' + m.elementDescription());
|
|
82
|
-
throw new AmbiguousElementError(`${description(interactor)} matches multiple elements:\n\n${alternatives.join('\n')}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Given a parent element, and an interactor, find all matching elements and
|
|
87
|
-
// return them. If no elements match, raise an error.
|
|
88
|
-
export function resolveNonEmpty(parentElement: Element, interactor: InteractorOptions<any, any, any>): Element[] {
|
|
89
|
-
return findMatchesNonEmpty(parentElement, interactor).map(m => m.element);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Given a parent element, and an interactor, check if there are any matching
|
|
93
|
-
// elements, and throw an error if there are. Otherwise return undefined.
|
|
94
|
-
export function resolveEmpty(parentElement: Element, interactor: InteractorOptions<any, any, any>): void {
|
|
95
|
-
let matching = findMatchesMatching(parentElement, interactor);
|
|
96
|
-
|
|
97
|
-
if(matching.length !== 0) {
|
|
98
|
-
let alternatives = matching.map((m) => '- ' + m.elementDescription());
|
|
99
|
-
throw new NotAbsentError(`${description(interactor)} exists but should not:\n\n${alternatives.join('\n')}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function ownDescription(options: InteractorOptions<any, any, any>): string {
|
|
104
|
-
if(options.locator) {
|
|
105
|
-
return `${options.name} ${options.locator.description} ${options.filter.description}`.trim();
|
|
106
|
-
} else {
|
|
107
|
-
return `${options.name} ${options.filter.description}`.trim();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function description(options: InteractorOptions<any, any, any>): string {
|
|
112
|
-
let ancestorsAndSelf: InteractorOptions<any, any, any>[] = options.ancestors.concat(options);
|
|
113
|
-
return ancestorsAndSelf.reverse().map(ownDescription).join(' within ');
|
|
114
|
-
}
|
|
115
29
|
|
|
116
30
|
/**
|
|
117
31
|
* Removes any default values for a filter from the lookup if that filter is present in the
|
|
@@ -132,59 +46,90 @@ function getLookupFilterForAssertion<E extends Element, F extends Filters<E>>(fi
|
|
|
132
46
|
return lookupFilter;
|
|
133
47
|
}
|
|
134
48
|
|
|
135
|
-
export function
|
|
136
|
-
return options.ancestors.reduce(resolveUnique, globals.document.documentElement);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function unsafeSyncResolveUnique<E extends Element>(options: InteractorOptions<E, any, any>): E {
|
|
140
|
-
return resolveUnique(unsafeSyncResolveParent(options), options) as E;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function instantiateBaseInteractor<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>>>>(
|
|
144
50
|
options: InteractorOptions<E, F, A>,
|
|
145
51
|
resolver: (options: InteractorOptions<E, F, A>) => E
|
|
146
|
-
):
|
|
52
|
+
): Interactor<E, FilterParams<E, F>> & FilterMethods<E, F> & ActionMethods<E, A, Interactor<E, FilterParams<E, F>>> {
|
|
147
53
|
let interactor = {
|
|
148
54
|
options,
|
|
149
55
|
|
|
150
56
|
get description(): string {
|
|
151
|
-
return
|
|
57
|
+
return formatDescription(options);
|
|
152
58
|
},
|
|
153
59
|
|
|
154
|
-
perform<T>(fn: (element: E) => T):
|
|
155
|
-
return
|
|
60
|
+
perform<T>(fn: (element: E) => T): ActionInteraction<E, T> {
|
|
61
|
+
return createInteraction('action', {
|
|
62
|
+
name: "perform",
|
|
63
|
+
interactor,
|
|
64
|
+
description: `run perform on ${formatDescription(options)}`,
|
|
65
|
+
run: (interactor) => converge(() => fn(resolver(interactor.options))),
|
|
66
|
+
});
|
|
156
67
|
},
|
|
157
68
|
|
|
158
|
-
assert(fn: (element: E) => void):
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
69
|
+
assert(fn: (element: E) => void): AssertionInteraction<E> {
|
|
70
|
+
return createInteraction('assertion', {
|
|
71
|
+
name: "assert",
|
|
72
|
+
interactor,
|
|
73
|
+
description: `${formatDescription(options)} asserts`,
|
|
74
|
+
run: (interactor) => converge(() => { fn(resolver(interactor.options)) }),
|
|
163
75
|
});
|
|
164
76
|
},
|
|
165
77
|
|
|
166
|
-
has(filters: FilterParams<E, F>):
|
|
78
|
+
has(filters: FilterParams<E, F>): AssertionInteraction<E> {
|
|
167
79
|
return interactor.is(filters);
|
|
168
80
|
},
|
|
169
81
|
|
|
170
|
-
is(filters: FilterParams<E, F>):
|
|
82
|
+
is(filters: FilterParams<E, F>): AssertionInteraction<E> {
|
|
171
83
|
let filter = new FilterSet(options.specification, filters);
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
84
|
+
return createInteraction('assertion', {
|
|
85
|
+
name: "is",
|
|
86
|
+
interactor,
|
|
87
|
+
filters,
|
|
88
|
+
description: `${formatDescription(options)} matches filters: ${filter.description}`,
|
|
89
|
+
run: (interactor) => converge(() => {
|
|
90
|
+
let element = resolver({...interactor.options, filter: getLookupFilterForAssertion(interactor.options.filter, filters) });
|
|
175
91
|
let match = new MatchFilter(element, filter);
|
|
176
92
|
if (!match.matches) {
|
|
177
|
-
throw new FilterNotMatchingError(`${
|
|
93
|
+
throw new FilterNotMatchingError(`${formatDescription(options)} does not match filters:\n\n${match.formatAsExpectations()}`);
|
|
178
94
|
}
|
|
179
|
-
})
|
|
95
|
+
}),
|
|
180
96
|
});
|
|
181
97
|
},
|
|
182
98
|
|
|
99
|
+
find<T extends Interactor<any, any>>(child: T): T {
|
|
100
|
+
return instantiateInteractor({
|
|
101
|
+
...child.options,
|
|
102
|
+
ancestors: [...options.ancestors, options, ...child.options.ancestors]
|
|
103
|
+
}, resolver) as unknown as T;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
exists(): AssertionInteraction<E> & FilterObject<boolean, Element> {
|
|
107
|
+
return createInteraction('assertion', {
|
|
108
|
+
name: 'exists',
|
|
109
|
+
interactor,
|
|
110
|
+
description: `${formatDescription(options)} exists`,
|
|
111
|
+
run: (interactor) => converge(() => {
|
|
112
|
+
resolveFirst(unsafeSyncResolveParent(interactor.options), options);
|
|
113
|
+
}),
|
|
114
|
+
}, (element) => hasMatchMatching(element, options));
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
absent(): AssertionInteraction<E> & FilterObject<boolean, Element> {
|
|
118
|
+
return createInteraction('assertion', {
|
|
119
|
+
name: 'absent',
|
|
120
|
+
interactor,
|
|
121
|
+
description: `${formatDescription(options)} does not exist`,
|
|
122
|
+
run: (interactor) => converge(() => {
|
|
123
|
+
resolveEmpty(unsafeSyncResolveParent(interactor.options), options);
|
|
124
|
+
}),
|
|
125
|
+
}, (element) => !hasMatchMatching(element, options));
|
|
126
|
+
},
|
|
127
|
+
|
|
183
128
|
apply(parentElement: Element): string {
|
|
184
129
|
let element = [...options.ancestors, options].reduce(resolveUnique, parentElement);
|
|
185
130
|
return applyFilter(options.specification.locator || defaultLocator, element);
|
|
186
131
|
}
|
|
187
|
-
}
|
|
132
|
+
};
|
|
188
133
|
|
|
189
134
|
for (let [actionName, action] of Object.entries(options.specification.actions || {})) {
|
|
190
135
|
if (!interactor.hasOwnProperty(actionName)) {
|
|
@@ -194,10 +139,13 @@ export function instantiateBaseInteractor<E extends Element, F extends Filters<E
|
|
|
194
139
|
if (args.length) {
|
|
195
140
|
actionDescription += ` with ` + args.map((a) => JSON.stringify(a)).join(', ');
|
|
196
141
|
}
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
142
|
+
return createInteraction('action', {
|
|
143
|
+
name: actionName,
|
|
144
|
+
interactor,
|
|
145
|
+
args,
|
|
146
|
+
description: `${actionDescription} on ${formatDescription(options)}`,
|
|
147
|
+
run: (interactor) => action(interactor as Interactor<E, FilterParams<E, F>> & FilterMethods<E, F> & ActionMethods<E, A, Interactor<E, FilterParams<E, F>>>, ...args)
|
|
148
|
+
});
|
|
201
149
|
},
|
|
202
150
|
configurable: true,
|
|
203
151
|
writable: true,
|
|
@@ -210,8 +158,11 @@ export function instantiateBaseInteractor<E extends Element, F extends Filters<E
|
|
|
210
158
|
if (!interactor.hasOwnProperty(filterName)) {
|
|
211
159
|
Object.defineProperty(interactor, filterName, {
|
|
212
160
|
value: function() {
|
|
213
|
-
return
|
|
214
|
-
|
|
161
|
+
return createInteraction('assertion', {
|
|
162
|
+
name: filterName,
|
|
163
|
+
interactor,
|
|
164
|
+
description: `${filterName} of ${formatDescription(options)}`,
|
|
165
|
+
run: (interactor) => converge(() => applyFilter(filter, resolver(interactor.options))),
|
|
215
166
|
}, (parentElement) => {
|
|
216
167
|
let element = [...options.ancestors, options].reduce(resolveUnique, parentElement);
|
|
217
168
|
return applyFilter(filter, element);
|
|
@@ -224,45 +175,10 @@ export function instantiateBaseInteractor<E extends Element, F extends Filters<E
|
|
|
224
175
|
}
|
|
225
176
|
}
|
|
226
177
|
|
|
227
|
-
return interactor as
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function instantiateInteractor<E extends Element, F extends Filters<E>, A extends Actions<E>>(
|
|
231
|
-
options: InteractorOptions<E, F, A>,
|
|
232
|
-
): Interactor<E, FilterParams<E, F>> & ActionMethods<E, A> {
|
|
233
|
-
let interactor = instantiateBaseInteractor(options, unsafeSyncResolveUnique)
|
|
234
|
-
|
|
235
|
-
return Object.assign(interactor, {
|
|
236
|
-
find<T extends Interactor<any, any>>(child: T): T {
|
|
237
|
-
return instantiateInteractor({
|
|
238
|
-
...child.options,
|
|
239
|
-
ancestors: [...options.ancestors, options, ...child.options.ancestors]
|
|
240
|
-
}) as unknown as T;
|
|
241
|
-
},
|
|
242
|
-
|
|
243
|
-
exists(): ReadonlyInteraction<void> & FilterObject<boolean, Element> {
|
|
244
|
-
return checkFilter(`${description(options)} exists`, () => {
|
|
245
|
-
return converge(() => {
|
|
246
|
-
resolveNonEmpty(unsafeSyncResolveParent(options), options);
|
|
247
|
-
});
|
|
248
|
-
}, (element) => {
|
|
249
|
-
return findMatchesMatching(element, options).length > 0;
|
|
250
|
-
});
|
|
251
|
-
},
|
|
252
|
-
|
|
253
|
-
absent(): ReadonlyInteraction<void> & FilterObject<boolean, Element> {
|
|
254
|
-
return checkFilter(`${description(options)} does not exist`, () => {
|
|
255
|
-
return converge(() => {
|
|
256
|
-
resolveEmpty(unsafeSyncResolveParent(options), options);
|
|
257
|
-
});
|
|
258
|
-
}, (element) => {
|
|
259
|
-
return findMatchesMatching(element, options).length === 0;
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
});
|
|
178
|
+
return interactor as Interactor<E, FilterParams<E, F>> & FilterMethods<E, F> & ActionMethods<E, A, Interactor<E, FilterParams<E, F>>>;
|
|
263
179
|
}
|
|
264
180
|
|
|
265
|
-
export function createConstructor<E extends Element, FP extends FilterParams<any, any>, FM extends FilterMethods<any, any>, AM extends ActionMethods<any, any>>(
|
|
181
|
+
export function createConstructor<E extends Element, FP extends FilterParams<any, any>, FM extends FilterMethods<any, any>, AM extends ActionMethods<any, any, any>>(
|
|
266
182
|
name: string,
|
|
267
183
|
specification: InteractorSpecification<E, any, any>,
|
|
268
184
|
): InteractorConstructor<E, FP, FM, AM> {
|
|
@@ -275,7 +191,7 @@ export function createConstructor<E extends Element, FP extends FilterParams<any
|
|
|
275
191
|
} else {
|
|
276
192
|
filter = new FilterSet(specification, args[0] || {});
|
|
277
193
|
}
|
|
278
|
-
return instantiateInteractor({ name, specification, filter, locator, ancestors: [] });
|
|
194
|
+
return instantiateInteractor({ name, specification, filter, locator, ancestors: [] }, unsafeSyncResolveUnique);
|
|
279
195
|
}
|
|
280
196
|
|
|
281
197
|
return Object.assign(initInteractor, {
|
|
@@ -288,7 +204,7 @@ export function createConstructor<E extends Element, FP extends FilterParams<any
|
|
|
288
204
|
filters: <FR extends Filters<E>>(filters: FR): InteractorConstructor<E, MergeObjects<FP, FilterParams<E, FR>>, MergeObjects<FM, FilterMethods<E, FR>>, AM> => {
|
|
289
205
|
return createConstructor(name, { ...specification, filters: { ...specification.filters, ...filters } });
|
|
290
206
|
},
|
|
291
|
-
actions: <AR extends Actions<E>>(actions: AR): InteractorConstructor<E, FP, FM, MergeObjects<AM, ActionMethods<E, AR>>> => {
|
|
207
|
+
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>>> => {
|
|
292
208
|
return createConstructor(name, { ...specification, actions: Object.assign({}, specification.actions, actions) });
|
|
293
209
|
},
|
|
294
210
|
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,12 +1,9 @@
|
|
|
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
|
-
export { perform } from './perform';
|
|
6
|
-
export { focused, focus, blur } from './focused';
|
|
7
5
|
export { isVisible } from 'element-is-visible';
|
|
8
6
|
export { Matcher } from './matcher';
|
|
9
|
-
export * from './dispatch';
|
|
10
7
|
|
|
11
8
|
export { including } from './matchers/including';
|
|
12
9
|
export { matching } from './matchers/matching';
|
|
@@ -16,5 +13,6 @@ export { not } from './matchers/not';
|
|
|
16
13
|
export { some } from './matchers/some';
|
|
17
14
|
export { every } from './matchers/every';
|
|
18
15
|
|
|
19
|
-
export { fillIn } from './element/fill-in';
|
|
20
16
|
export { click } from './element/click';
|
|
17
|
+
export { MergeObjects } from './merge-objects'
|
|
18
|
+
export { MaybeMatcher } from './matcher'
|
package/src/inspector.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { instantiateInteractor } from "./constructor";
|
|
4
|
+
import { findElements, unsafeSyncResolveParent } from './resolvers'
|
|
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
|
|
7
8
|
type GetFilters<I extends InteractorConstructor<any, any, any, any>> = I extends InteractorConstructor<any, infer F, any, any> ? F : never
|
|
8
9
|
type GetActions<I extends InteractorConstructor<any, any, any, any>> = I extends InteractorConstructor<any, any, any, infer A> ? A : never
|
|
9
10
|
|
|
10
|
-
export interface InteractorInspector<E extends Element, FP extends FilterParams<any, any>> extends
|
|
11
|
+
export interface InteractorInspector<E extends Element, FP extends FilterParams<any, any>> extends Omit<Interactor<E, FP>, 'find'> {
|
|
11
12
|
element: E,
|
|
12
13
|
find<T extends InteractorConstructor<any, any, any, any>>(interactor: T): Inspector<T>
|
|
13
14
|
}
|
|
@@ -30,7 +31,7 @@ export function createInspector<IC extends InteractorConstructor<any, any, any,
|
|
|
30
31
|
let elements = findElements<GetElement<IC>>(parentElement ?? unsafeSyncResolveParent(options), options);
|
|
31
32
|
return elements.map(
|
|
32
33
|
element => (Object.assign(
|
|
33
|
-
|
|
34
|
+
instantiateInteractor(options, () => element) as (Interactor<GetElement<IC>, Filters<GetFilters<IC>>> & GetActions<IC>), {
|
|
34
35
|
element,
|
|
35
36
|
find<T extends InteractorConstructor<any, any, any, any>>(constructor: T): Inspector<T> {
|
|
36
37
|
return createInspector(constructor, element)
|
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>;
|
|
47
|
+
|
|
48
|
+
halt: () => Promise<void>;
|
|
30
49
|
|
|
31
|
-
[interactionSymbol]: true
|
|
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,72 @@ 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
|
+
|
|
87
|
+
function operation(scope: Task): Operation<T> {
|
|
88
|
+
let run = () => options.run(options.interactor);
|
|
89
|
+
|
|
90
|
+
return (globals.wrapInteraction
|
|
91
|
+
? globals.wrapInteraction(interaction, () => scope.run(run))
|
|
92
|
+
: globals.wrapAction(interaction.description, () => scope.run(run), type)) as Operation<T>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function action(): Task<T> {
|
|
96
|
+
if(!task) {
|
|
97
|
+
task = run(operation);
|
|
98
|
+
}
|
|
99
|
+
return task;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
let serializedOptions = serializeInteractionOptions(type, options)
|
|
103
|
+
let interaction: Interaction<E, T> & Operation<T> = {
|
|
104
|
+
type,
|
|
105
|
+
options: serializedOptions,
|
|
106
|
+
description: options.description,
|
|
107
|
+
interactor: options.interactor,
|
|
108
|
+
run: options.run,
|
|
50
109
|
action,
|
|
110
|
+
check: type === 'assertion' ? action : undefined,
|
|
111
|
+
code: () => serializedOptions.code(),
|
|
112
|
+
halt: () => action().halt(),
|
|
51
113
|
[interactionSymbol]: true,
|
|
52
|
-
[Symbol.toStringTag]: `[interaction ${description}]`,
|
|
114
|
+
[Symbol.toStringTag]: `[interaction ${options.description}]`,
|
|
115
|
+
[Symbol.operation]: operation,
|
|
53
116
|
then(onFulfill, onReject) {
|
|
54
|
-
|
|
55
|
-
return promise.then(onFulfill, onReject);
|
|
117
|
+
return action().then(onFulfill, onReject);
|
|
56
118
|
},
|
|
57
119
|
catch(onReject) {
|
|
58
|
-
|
|
59
|
-
return promise.catch(onReject);
|
|
120
|
+
return action().catch(onReject);
|
|
60
121
|
},
|
|
61
122
|
finally(handler) {
|
|
62
|
-
|
|
63
|
-
return promise.finally(handler);
|
|
123
|
+
return action().finally(handler);
|
|
64
124
|
}
|
|
65
125
|
}
|
|
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) };
|
|
126
|
+
if (apply) {
|
|
127
|
+
return Object.assign(interaction, { apply });
|
|
128
|
+
} else {
|
|
129
|
+
return interaction;
|
|
130
|
+
}
|
|
82
131
|
}
|