@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/constructor.js +62 -126
  3. package/dist/cjs/constructor.js.map +1 -1
  4. package/dist/cjs/converge.js +3 -5
  5. package/dist/cjs/converge.js.map +1 -1
  6. package/dist/cjs/format.js +50 -0
  7. package/dist/cjs/format.js.map +1 -0
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/inspector.js +2 -1
  10. package/dist/cjs/inspector.js.map +1 -1
  11. package/dist/cjs/interaction.js +59 -35
  12. package/dist/cjs/interaction.js.map +1 -1
  13. package/dist/cjs/match.js +17 -7
  14. package/dist/cjs/match.js.map +1 -1
  15. package/dist/cjs/resolvers.js +98 -0
  16. package/dist/cjs/resolvers.js.map +1 -0
  17. package/dist/cjs/serialize.js +48 -0
  18. package/dist/cjs/serialize.js.map +1 -0
  19. package/dist/constructor.d.ts +2 -8
  20. package/dist/constructor.d.ts.map +1 -1
  21. package/dist/converge.d.ts +2 -1
  22. package/dist/converge.d.ts.map +1 -1
  23. package/dist/esm/constructor.js +59 -117
  24. package/dist/esm/constructor.js.map +1 -1
  25. package/dist/esm/converge.js +3 -5
  26. package/dist/esm/converge.js.map +1 -1
  27. package/dist/esm/{format-table.js → format.js} +19 -1
  28. package/dist/esm/format.js.map +1 -0
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/inspector.js +2 -1
  31. package/dist/esm/inspector.js.map +1 -1
  32. package/dist/esm/interaction.js +56 -29
  33. package/dist/esm/interaction.js.map +1 -1
  34. package/dist/esm/match.js +17 -7
  35. package/dist/esm/match.js.map +1 -1
  36. package/dist/esm/resolvers.js +88 -0
  37. package/dist/esm/resolvers.js.map +1 -0
  38. package/dist/esm/serialize.js +43 -0
  39. package/dist/esm/serialize.js.map +1 -0
  40. package/dist/format.d.ts +10 -0
  41. package/dist/format.d.ts.map +1 -0
  42. package/dist/index.d.ts +4 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/inspector.d.ts.map +1 -1
  45. package/dist/interaction.d.ts +38 -10
  46. package/dist/interaction.d.ts.map +1 -1
  47. package/dist/match.d.ts +5 -5
  48. package/dist/match.d.ts.map +1 -1
  49. package/dist/resolvers.d.ts +9 -0
  50. package/dist/resolvers.d.ts.map +1 -0
  51. package/dist/serialize.d.ts +13 -0
  52. package/dist/serialize.d.ts.map +1 -0
  53. package/dist/specification.d.ts +20 -17
  54. package/dist/specification.d.ts.map +1 -1
  55. package/dist/tsconfig.build.tsbuildinfo +1 -1
  56. package/package.json +3 -2
  57. package/src/constructor.ts +70 -138
  58. package/src/converge.ts +3 -6
  59. package/src/format.ts +58 -0
  60. package/src/index.ts +4 -2
  61. package/src/inspector.ts +2 -1
  62. package/src/interaction.ts +101 -36
  63. package/src/match.ts +23 -15
  64. package/src/resolvers.ts +100 -0
  65. package/src/serialize.ts +54 -0
  66. package/src/specification.ts +22 -19
  67. package/dist/cjs/format-table.js +0 -30
  68. package/dist/cjs/format-table.js.map +0 -1
  69. package/dist/esm/format-table.js.map +0 -1
  70. package/dist/format-table.d.ts +0 -6
  71. package/dist/format-table.d.ts.map +0 -1
  72. package/src/format-table.ts +0 -34
@@ -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 { formatTable } from './format-table';
21
+ import { formatDescription } from './format';
23
22
  import { FilterNotMatchingError } from './errors';
24
- import { interaction, interactionFilter, check, checkFilter, Interaction, ReadonlyInteraction } from './interaction';
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 unsafeSyncResolveParent(options: InteractorOptions<any, any, any>): Element {
135
- return options.ancestors.reduce(resolveUnique, globals.document.documentElement);
136
- }
137
-
138
- export function unsafeSyncResolveUnique<E extends Element>(options: InteractorOptions<E, any, any>): E {
139
- return resolveUnique(unsafeSyncResolveParent(options), options) as E;
140
- }
141
-
142
- 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 description(options);
57
+ return formatDescription(options);
151
58
  },
152
59
 
153
- perform<T>(fn: (element: E) => T): Interaction<T> {
154
- return interaction(`run perform on ${description(options)}`, () => converge(() => fn(resolver(options))))
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): ReadonlyInteraction<void> {
158
- return check(`${description(options)} asserts`, () => {
159
- return converge(() => {
160
- fn(resolver(options));
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>): ReadonlyInteraction<void> {
80
+ has(filters: FilterParams<E, F>): AssertionInteraction<E> {
166
81
  return interactor.is(filters);
167
82
  },
168
83
 
169
- is(filters: FilterParams<E, F>): ReadonlyInteraction<void> {
84
+ is(filters: FilterParams<E, F>): AssertionInteraction<E> {
170
85
  let filter = new FilterSet(options.specification, filters);
171
- return check(`${description(options)} matches filters: ${filter.description}`, () => {
172
- return converge(() => {
173
- let element = resolver({...options, filter: getLookupFilterForAssertion(options.filter, filters) });
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(`${description(options)} does not match filters:\n\n${match.formatAsExpectations()}`);
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(): ReadonlyInteraction<void> & FilterObject<boolean, Element> {
190
- return checkFilter(`${description(options)} exists`, () => {
191
- return converge(() => {
192
- resolveNonEmpty(unsafeSyncResolveParent(options), options);
193
- });
194
- }, (element) => {
195
- return findMatchesMatching(element, options).length > 0;
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(): ReadonlyInteraction<void> & FilterObject<boolean, Element> {
200
- return checkFilter(`${description(options)} does not exist`, () => {
201
- return converge(() => {
202
- resolveEmpty(unsafeSyncResolveParent(options), options);
203
- });
204
- }, (element) => {
205
- return findMatchesMatching(element, options).length === 0;
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 interaction(
224
- `${actionDescription} on ${description(options)}`,
225
- () => action(interactor as Interactor<E, FilterParams<E, F>> & ActionMethods<E, A>, ...args)
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 interactionFilter(`${filterName} of ${description(options)}`, async () => {
240
- return converge(() => applyFilter(filter, resolver(options)));
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 wait(ms: number) {
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
- await wait(1);
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, ReadonlyInteraction, isInteraction } from './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 { findElements, unsafeSyncResolveParent, instantiateInteractor } from "./constructor";
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
@@ -1,9 +1,12 @@
1
- import { globals } from '@interactors/globals';
2
- import type { FilterObject } from './specification';
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: () => Promise<T>;
44
+ action: () => Task<T>;
45
+
46
+ check?: () => Task<T>;
30
47
 
31
- [interactionSymbol]: true
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 ReadonlyInteraction<T> extends Interaction<T> {
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: () => Promise<T>;
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>(description: string, action: () => Promise<T>): Interaction<T> {
47
- let promise: Promise<T>;
48
- return {
49
- description,
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
- if(!promise) { promise = this.action(); }
55
- return promise.then(onFulfill, onReject);
133
+ return action().then(onFulfill, catchHalt(onReject));
56
134
  },
57
135
  catch(onReject) {
58
- if(!promise) { promise = this.action(); }
59
- return promise.catch(onReject);
136
+ return action().catch(catchHalt(onReject));
60
137
  },
61
138
  finally(handler) {
62
- if(!promise) { promise = this.action(); }
63
- return promise.finally(handler);
139
+ return action().finally(handler);
64
140
  }
65
141
  }
66
- }
67
-
68
- export function interaction<T>(description: string, action: () => Promise<T>): Interaction<T> {
69
- return createInteraction(description, globals.wrapAction(description, action, 'interaction'))
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
  }