@interactors/core 0.4.0 → 1.0.0-effection.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/constructor.js +55 -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 +54 -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 +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 +52 -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 +51 -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 -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/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 +19 -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 +63 -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 +96 -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 +21 -19
- package/CHANGELOG.md +0 -27
- 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/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>;
|
|
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,83 @@ 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(): Operation<T> {
|
|
88
|
+
let run = () => options.run(options.interactor);
|
|
89
|
+
let operation = globals.wrapInteraction
|
|
90
|
+
? globals.wrapInteraction(interaction, async () => run)
|
|
91
|
+
: globals.wrapAction(interaction.description, async () => run, type)
|
|
92
|
+
|
|
93
|
+
return function* () {
|
|
94
|
+
// NOTE: If effection gets Promise it will fulfill and treat the result as ended value
|
|
95
|
+
// NOTE: Sometimes original interaction function might be wrapped multiple times into async function
|
|
96
|
+
// NOTE: That means effection returns the original interaction function from a wrapped one instead of executing it
|
|
97
|
+
// NOTE: We `yield` operation until we get the `run` function which returns a generator
|
|
98
|
+
// NOTE: Then we yield it and return a result
|
|
99
|
+
while (operation != run) {
|
|
100
|
+
operation = yield operation
|
|
101
|
+
}
|
|
102
|
+
return yield operation;
|
|
103
|
+
} as Operation<T>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function action(): Task<T> {
|
|
107
|
+
if(!task) {
|
|
108
|
+
task = run(operation);
|
|
109
|
+
}
|
|
110
|
+
return task;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let serializedOptions = serializeInteractionOptions(type, options)
|
|
114
|
+
let interaction: Interaction<E, T> & Operation<T> = {
|
|
115
|
+
type,
|
|
116
|
+
options: serializedOptions,
|
|
117
|
+
description: options.description,
|
|
118
|
+
interactor: options.interactor,
|
|
119
|
+
run: options.run,
|
|
50
120
|
action,
|
|
121
|
+
check: type === 'assertion' ? action : undefined,
|
|
122
|
+
code: () => serializedOptions.code(),
|
|
123
|
+
halt: () => action().halt(),
|
|
51
124
|
[interactionSymbol]: true,
|
|
52
|
-
[Symbol.toStringTag]: `[interaction ${description}]`,
|
|
125
|
+
[Symbol.toStringTag]: `[interaction ${options.description}]`,
|
|
126
|
+
[Symbol.operation]: operation,
|
|
53
127
|
then(onFulfill, onReject) {
|
|
54
|
-
|
|
55
|
-
return promise.then(onFulfill, onReject);
|
|
128
|
+
return action().then(onFulfill, onReject);
|
|
56
129
|
},
|
|
57
130
|
catch(onReject) {
|
|
58
|
-
|
|
59
|
-
return promise.catch(onReject);
|
|
131
|
+
return action().catch(onReject);
|
|
60
132
|
},
|
|
61
133
|
finally(handler) {
|
|
62
|
-
|
|
63
|
-
return promise.finally(handler);
|
|
134
|
+
return action().finally(handler);
|
|
64
135
|
}
|
|
65
136
|
}
|
|
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) };
|
|
137
|
+
if (apply) {
|
|
138
|
+
return Object.assign(interaction, { apply });
|
|
139
|
+
} else {
|
|
140
|
+
return interaction;
|
|
141
|
+
}
|
|
82
142
|
}
|
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
|
}
|
package/src/matcher.ts
CHANGED
|
@@ -3,6 +3,7 @@ import isEqual from 'lodash.isequal';
|
|
|
3
3
|
export interface Matcher<T> {
|
|
4
4
|
match(actual: T): boolean;
|
|
5
5
|
description(): string;
|
|
6
|
+
code?(): string;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export type MaybeMatcher<T> = Matcher<T> | T;
|
|
@@ -26,3 +27,13 @@ export function applyMatcher<T>(value: MaybeMatcher<T>, actual: T): boolean {
|
|
|
26
27
|
return isEqual(value, actual);
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
|
|
31
|
+
export function matcherCode<T>(value: MaybeMatcher<T>): string {
|
|
32
|
+
if (isMatcher(value) && value.code) {
|
|
33
|
+
return value.code();
|
|
34
|
+
} else if (value instanceof RegExp) {
|
|
35
|
+
return value.toString();
|
|
36
|
+
} else {
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/matchers/and.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher } from '../matcher';
|
|
1
|
+
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher, matcherCode } from '../matcher';
|
|
2
2
|
|
|
3
3
|
export function and<T>(...args: MaybeMatcher<T>[]): Matcher<T> {
|
|
4
4
|
return {
|
|
@@ -8,5 +8,8 @@ export function and<T>(...args: MaybeMatcher<T>[]): Matcher<T> {
|
|
|
8
8
|
description(): string {
|
|
9
9
|
return args.map(matcherDescription).join(' and ');
|
|
10
10
|
},
|
|
11
|
+
code(): string {
|
|
12
|
+
return `and(${args.map(arg => matcherCode(arg)).join(', ')})`
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
}
|
package/src/matchers/every.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Matcher, MaybeMatcher, applyMatcher, matcherDescription } from '../matcher';
|
|
1
|
+
import { Matcher, MaybeMatcher, applyMatcher, matcherDescription, matcherCode } from '../matcher';
|
|
2
2
|
|
|
3
3
|
export function every<T>(expected: MaybeMatcher<T>): Matcher<Iterable<T>> {
|
|
4
4
|
return {
|
|
@@ -8,5 +8,8 @@ export function every<T>(expected: MaybeMatcher<T>): Matcher<Iterable<T>> {
|
|
|
8
8
|
description(): string {
|
|
9
9
|
return `every item ${matcherDescription(expected)}`;
|
|
10
10
|
},
|
|
11
|
+
code(): string {
|
|
12
|
+
return `every(${matcherCode(expected)})`
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Matcher } from '../matcher';
|
|
1
|
+
import { Matcher, matcherCode} from '../matcher';
|
|
2
2
|
|
|
3
3
|
export function including(subString: string): Matcher<string> {
|
|
4
4
|
return {
|
|
@@ -8,5 +8,8 @@ export function including(subString: string): Matcher<string> {
|
|
|
8
8
|
description(): string {
|
|
9
9
|
return `including ${JSON.stringify(subString)}`;
|
|
10
10
|
},
|
|
11
|
+
code(): string {
|
|
12
|
+
return `including(${matcherCode(subString)})`
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
}
|
package/src/matchers/matching.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Matcher } from '../matcher';
|
|
1
|
+
import { Matcher, matcherCode } from '../matcher';
|
|
2
2
|
|
|
3
3
|
export function matching(regexp: RegExp): Matcher<string> {
|
|
4
4
|
return {
|
|
@@ -8,5 +8,8 @@ export function matching(regexp: RegExp): Matcher<string> {
|
|
|
8
8
|
description(): string {
|
|
9
9
|
return `matching ${regexp}`;
|
|
10
10
|
},
|
|
11
|
+
code(): string {
|
|
12
|
+
return `matching(${matcherCode(regexp)})`
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
}
|
package/src/matchers/not.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher } from '../matcher';
|
|
1
|
+
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher, matcherCode } from '../matcher';
|
|
2
2
|
|
|
3
3
|
export function not<T>(matcher: MaybeMatcher<T>): Matcher<T> {
|
|
4
4
|
return {
|
|
@@ -8,5 +8,8 @@ export function not<T>(matcher: MaybeMatcher<T>): Matcher<T> {
|
|
|
8
8
|
description(): string {
|
|
9
9
|
return `not ${matcherDescription(matcher)}`;
|
|
10
10
|
},
|
|
11
|
+
code(): string {
|
|
12
|
+
return `not(${matcherCode(matcher)})`
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
}
|
package/src/matchers/or.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher } from '../matcher';
|
|
1
|
+
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher, matcherCode } from '../matcher';
|
|
2
2
|
|
|
3
3
|
export function or<T>(...args: MaybeMatcher<T>[]): Matcher<T> {
|
|
4
4
|
return {
|
|
@@ -8,5 +8,8 @@ export function or<T>(...args: MaybeMatcher<T>[]): Matcher<T> {
|
|
|
8
8
|
description(): string {
|
|
9
9
|
return args.map(matcherDescription).join(' or ');
|
|
10
10
|
},
|
|
11
|
+
code(): string {
|
|
12
|
+
return `or(${args.map(arg => matcherCode(arg)).join(', ')})`
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
}
|
package/src/matchers/some.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Matcher, MaybeMatcher, applyMatcher, matcherDescription } from '../matcher';
|
|
1
|
+
import { Matcher, MaybeMatcher, applyMatcher, matcherDescription, matcherCode } from '../matcher';
|
|
2
2
|
|
|
3
3
|
export function some<T>(expected: MaybeMatcher<T>): Matcher<Iterable<T>> {
|
|
4
4
|
return {
|
|
@@ -8,5 +8,8 @@ export function some<T>(expected: MaybeMatcher<T>): Matcher<Iterable<T>> {
|
|
|
8
8
|
description(): string {
|
|
9
9
|
return `some item ${matcherDescription(expected)}`;
|
|
10
10
|
},
|
|
11
|
+
code(): string {
|
|
12
|
+
return `some(${matcherCode(expected)})`
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
}
|
package/src/resolvers.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
|
|
4
|
+
import { globals } from '@interactors/globals';
|
|
5
|
+
import { Match } from './match';
|
|
6
|
+
import { NoSuchElementError, NotAbsentError, AmbiguousElementError } from './errors';
|
|
7
|
+
import { formatDescription, formatMatchesTable } from './format';
|
|
8
|
+
import { InteractorOptions } from './specification';
|
|
9
|
+
|
|
10
|
+
const defaultSelector = 'div';
|
|
11
|
+
|
|
12
|
+
export function findElements<E extends Element>(parentElement: Element, interactor: InteractorOptions<any, any, any>): E[] {
|
|
13
|
+
if (!interactor.name) {
|
|
14
|
+
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\')');
|
|
15
|
+
}
|
|
16
|
+
if (typeof interactor.specification.selector === 'function') {
|
|
17
|
+
return interactor.specification.selector(parentElement);
|
|
18
|
+
} else if (interactor.specification.selector === ':root') {
|
|
19
|
+
// this is a bit of a hack, because otherwise there isn't a good way of selecting the root element
|
|
20
|
+
return [parentElement.ownerDocument.querySelector(':root') as E];
|
|
21
|
+
} else {
|
|
22
|
+
return Array.from(parentElement.querySelectorAll(interactor.specification.selector || defaultSelector));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function findMatches(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any>[] {
|
|
27
|
+
return findElements(parentElement, interactor).map((e) => new Match(e, interactor.filter, interactor.locator));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function throwNoSuchElementError(matches: Match<Element, any>[], interactor: InteractorOptions<any, any, any>): never {
|
|
31
|
+
if (matches.length === 0) {
|
|
32
|
+
throw new NoSuchElementError(`did not find ${formatDescription(interactor)}`);
|
|
33
|
+
} else {
|
|
34
|
+
throw new NoSuchElementError(`did not find ${formatDescription(interactor)}, did you mean one of:\n\n${formatMatchesTable(interactor, matches)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findFirstMatch(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any> {
|
|
39
|
+
let matches: Match<Element, any>[] = []
|
|
40
|
+
for (let element of findElements(parentElement, interactor)) {
|
|
41
|
+
let match = new Match(element, interactor.filter, interactor.locator)
|
|
42
|
+
matches.push(match)
|
|
43
|
+
if (match.matches) {
|
|
44
|
+
return match
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throwNoSuchElementError(matches, interactor);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hasMatchMatching(parentElement: Element, interactor: InteractorOptions<any, any, any>): boolean {
|
|
51
|
+
return findElements(parentElement, interactor).some(element => new Match(element, interactor.filter, interactor.locator).matches);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findMatchesNonEmpty(parentElement: Element, interactor: InteractorOptions<any, any, any>): Match<Element, any>[] {
|
|
55
|
+
let matches = findMatches(parentElement, interactor);
|
|
56
|
+
let matching = matches.filter((m) => m.matches);
|
|
57
|
+
if (matching.length > 0) {
|
|
58
|
+
return matching;
|
|
59
|
+
}
|
|
60
|
+
throwNoSuchElementError(matches, interactor);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Given a parent element, and an interactor, find exactly one matching element
|
|
64
|
+
// and return it. If no elements match, raise an error. If more than one
|
|
65
|
+
// element matches, raise an error.
|
|
66
|
+
export function resolveUnique(parentElement: Element, interactor: InteractorOptions<any, any, any>): Element {
|
|
67
|
+
let matching = findMatchesNonEmpty(parentElement, interactor);
|
|
68
|
+
|
|
69
|
+
if (matching.length === 1) {
|
|
70
|
+
return matching[0].element;
|
|
71
|
+
} else {
|
|
72
|
+
let alternatives = matching.map((m) => '- ' + m.elementDescription());
|
|
73
|
+
throw new AmbiguousElementError(`${formatDescription(interactor)} matches multiple elements:\n\n${alternatives.join('\n')}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Given a parent element, and an interactor, find the first matching element and
|
|
78
|
+
// return it. If no elements match, raise an error.
|
|
79
|
+
export function resolveFirst(parentElement: Element, interactor: InteractorOptions<any, any, any>): Element {
|
|
80
|
+
return findFirstMatch(parentElement, interactor).element
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Given a parent element, and an interactor, check if there are any matching
|
|
84
|
+
// elements, and throw an error if there are. Otherwise return undefined.
|
|
85
|
+
export function resolveEmpty(parentElement: Element, interactor: InteractorOptions<any, any, any>): void {
|
|
86
|
+
let matching = findMatches(parentElement, interactor).filter((m) => m.matches)
|
|
87
|
+
|
|
88
|
+
if (matching.length !== 0) {
|
|
89
|
+
let alternatives = matching.map((m) => '- ' + m.elementDescription());
|
|
90
|
+
throw new NotAbsentError(`${formatDescription(interactor)} exists but should not:\n\n${alternatives.join('\n')}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function unsafeSyncResolveParent(options: InteractorOptions<any, any, any>): Element {
|
|
95
|
+
return options.ancestors.reduce(resolveUnique, globals.document.documentElement);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function unsafeSyncResolveUnique<E extends Element>(options: InteractorOptions<E, any, any>): E {
|
|
99
|
+
return resolveUnique(unsafeSyncResolveParent(options), options) as E;
|
|
100
|
+
}
|
package/src/serialize.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { InteractionOptions as SerializedInteractionOptions, InteractionType } from "@interactors/globals";
|
|
3
|
+
import { pascalCase } from "change-case";
|
|
4
|
+
import { InteractionOptions } from "./interaction";
|
|
5
|
+
import { matcherCode } from "./matcher";
|
|
6
|
+
import { InteractorOptions } from "./specification";
|
|
7
|
+
|
|
8
|
+
export function serializeInteractorOptions(options: InteractorOptions<any, any, any>): {
|
|
9
|
+
interactor: string;
|
|
10
|
+
filter: { [name: string]: unknown };
|
|
11
|
+
locator: string;
|
|
12
|
+
code: () => string;
|
|
13
|
+
} {
|
|
14
|
+
let locator = matcherCode(options.locator?.value);
|
|
15
|
+
return {
|
|
16
|
+
interactor: options.name,
|
|
17
|
+
filter: options.filter.all,
|
|
18
|
+
locator,
|
|
19
|
+
code() {
|
|
20
|
+
let interactorName = pascalCase(options.name);
|
|
21
|
+
let filters = Object.entries(options.filter.all).map(([name, filter]) => `"${name}": ${matcherCode(filter)}`);
|
|
22
|
+
return `${interactorName}(${[locator, filters.length && `{ ${filters.join(", ")} }`]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join(", ")})`;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function serializeInteractionOptions<E extends Element, T>(
|
|
30
|
+
type: InteractionType,
|
|
31
|
+
{ name, interactor: { options }, args, filters }: InteractionOptions<E, T>
|
|
32
|
+
): SerializedInteractionOptions {
|
|
33
|
+
let interactor = serializeInteractorOptions(options);
|
|
34
|
+
let ancestors = options.ancestors.map((ancestor) => serializeInteractorOptions(ancestor));
|
|
35
|
+
return {
|
|
36
|
+
...interactor,
|
|
37
|
+
ancestors,
|
|
38
|
+
name,
|
|
39
|
+
type,
|
|
40
|
+
args: filters ? [filters] : args ? args : undefined,
|
|
41
|
+
code() {
|
|
42
|
+
let serializedArgs = "";
|
|
43
|
+
if (filters) {
|
|
44
|
+
let serializedFilters = Object.entries(filters).map(([name, filter]) => `"${name}": ${matcherCode(filter)}`);
|
|
45
|
+
serializedArgs = `{ ${serializedFilters.join(", ")} }`;
|
|
46
|
+
} else if (args) {
|
|
47
|
+
serializedArgs = args.map((arg) => JSON.stringify(arg)).join(", ");
|
|
48
|
+
}
|
|
49
|
+
return `${[...ancestors, interactor]
|
|
50
|
+
.map(({ code }, index) => (index == 0 ? code() : `${code()})`))
|
|
51
|
+
.join(".find(")}.${name}(${serializedArgs})`;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|