@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.
Files changed (152) hide show
  1. package/dist/cjs/constructor.js +66 -143
  2. package/dist/cjs/constructor.js.map +1 -1
  3. package/dist/cjs/converge.js +3 -5
  4. package/dist/cjs/converge.js.map +1 -1
  5. package/dist/cjs/format.js +50 -0
  6. package/dist/cjs/format.js.map +1 -0
  7. package/dist/cjs/index.js +1 -20
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/inspector.js +3 -2
  10. package/dist/cjs/inspector.js.map +1 -1
  11. package/dist/cjs/interaction.js +43 -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/matcher.js +13 -1
  16. package/dist/cjs/matcher.js.map +1 -1
  17. package/dist/cjs/matchers/and.js +3 -0
  18. package/dist/cjs/matchers/and.js.map +1 -1
  19. package/dist/cjs/matchers/every.js +3 -0
  20. package/dist/cjs/matchers/every.js.map +1 -1
  21. package/dist/cjs/matchers/including.js +4 -0
  22. package/dist/cjs/matchers/including.js.map +1 -1
  23. package/dist/cjs/matchers/matching.js +4 -0
  24. package/dist/cjs/matchers/matching.js.map +1 -1
  25. package/dist/cjs/matchers/not.js +3 -0
  26. package/dist/cjs/matchers/not.js.map +1 -1
  27. package/dist/cjs/matchers/or.js +3 -0
  28. package/dist/cjs/matchers/or.js.map +1 -1
  29. package/dist/cjs/matchers/some.js +3 -0
  30. package/dist/cjs/matchers/some.js.map +1 -1
  31. package/dist/cjs/resolvers.js +98 -0
  32. package/dist/cjs/resolvers.js.map +1 -0
  33. package/dist/cjs/serialize.js +48 -0
  34. package/dist/cjs/serialize.js.map +1 -0
  35. package/dist/constructor.d.ts +3 -10
  36. package/dist/constructor.d.ts.map +1 -1
  37. package/dist/converge.d.ts +2 -1
  38. package/dist/converge.d.ts.map +1 -1
  39. package/dist/esm/constructor.js +64 -134
  40. package/dist/esm/constructor.js.map +1 -1
  41. package/dist/esm/converge.js +3 -5
  42. package/dist/esm/converge.js.map +1 -1
  43. package/dist/esm/{format-table.js → format.js} +19 -1
  44. package/dist/esm/format.js.map +1 -0
  45. package/dist/esm/index.js +0 -4
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/inspector.js +3 -2
  48. package/dist/esm/inspector.js.map +1 -1
  49. package/dist/esm/interaction.js +40 -29
  50. package/dist/esm/interaction.js.map +1 -1
  51. package/dist/esm/match.js +17 -7
  52. package/dist/esm/match.js.map +1 -1
  53. package/dist/esm/matcher.js +11 -0
  54. package/dist/esm/matcher.js.map +1 -1
  55. package/dist/esm/matchers/and.js +4 -1
  56. package/dist/esm/matchers/and.js.map +1 -1
  57. package/dist/esm/matchers/every.js +4 -1
  58. package/dist/esm/matchers/every.js.map +1 -1
  59. package/dist/esm/matchers/including.js +4 -0
  60. package/dist/esm/matchers/including.js.map +1 -1
  61. package/dist/esm/matchers/matching.js +4 -0
  62. package/dist/esm/matchers/matching.js.map +1 -1
  63. package/dist/esm/matchers/not.js +4 -1
  64. package/dist/esm/matchers/not.js.map +1 -1
  65. package/dist/esm/matchers/or.js +4 -1
  66. package/dist/esm/matchers/or.js.map +1 -1
  67. package/dist/esm/matchers/some.js +4 -1
  68. package/dist/esm/matchers/some.js.map +1 -1
  69. package/dist/esm/resolvers.js +88 -0
  70. package/dist/esm/resolvers.js.map +1 -0
  71. package/dist/esm/serialize.js +43 -0
  72. package/dist/esm/serialize.js.map +1 -0
  73. package/dist/format.d.ts +10 -0
  74. package/dist/format.d.ts.map +1 -0
  75. package/dist/index.d.ts +4 -6
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/inspector.d.ts +2 -2
  78. package/dist/inspector.d.ts.map +1 -1
  79. package/dist/interaction.d.ts +38 -10
  80. package/dist/interaction.d.ts.map +1 -1
  81. package/dist/match.d.ts +5 -5
  82. package/dist/match.d.ts.map +1 -1
  83. package/dist/matcher.d.ts +2 -0
  84. package/dist/matcher.d.ts.map +1 -1
  85. package/dist/matchers/and.d.ts.map +1 -1
  86. package/dist/matchers/every.d.ts.map +1 -1
  87. package/dist/matchers/including.d.ts.map +1 -1
  88. package/dist/matchers/matching.d.ts.map +1 -1
  89. package/dist/matchers/not.d.ts.map +1 -1
  90. package/dist/matchers/or.d.ts.map +1 -1
  91. package/dist/matchers/some.d.ts.map +1 -1
  92. package/dist/resolvers.d.ts +9 -0
  93. package/dist/resolvers.d.ts.map +1 -0
  94. package/dist/serialize.d.ts +13 -0
  95. package/dist/serialize.d.ts.map +1 -0
  96. package/dist/specification.d.ts +49 -51
  97. package/dist/specification.d.ts.map +1 -1
  98. package/dist/tsconfig.build.tsbuildinfo +1 -1
  99. package/package.json +3 -3
  100. package/src/constructor.ts +76 -160
  101. package/src/converge.ts +3 -6
  102. package/src/format.ts +58 -0
  103. package/src/index.ts +4 -6
  104. package/src/inspector.ts +5 -4
  105. package/src/interaction.ts +85 -36
  106. package/src/match.ts +23 -15
  107. package/src/matcher.ts +11 -0
  108. package/src/matchers/and.ts +4 -1
  109. package/src/matchers/every.ts +4 -1
  110. package/src/matchers/including.ts +4 -1
  111. package/src/matchers/matching.ts +4 -1
  112. package/src/matchers/not.ts +4 -1
  113. package/src/matchers/or.ts +4 -1
  114. package/src/matchers/some.ts +4 -1
  115. package/src/resolvers.ts +100 -0
  116. package/src/serialize.ts +54 -0
  117. package/src/specification.ts +58 -61
  118. package/CHANGELOG.md +0 -14
  119. package/dist/cjs/dispatch.js +0 -34
  120. package/dist/cjs/dispatch.js.map +0 -1
  121. package/dist/cjs/element/fill-in.js +0 -71
  122. package/dist/cjs/element/fill-in.js.map +0 -1
  123. package/dist/cjs/focused.js +0 -47
  124. package/dist/cjs/focused.js.map +0 -1
  125. package/dist/cjs/format-table.js +0 -30
  126. package/dist/cjs/format-table.js.map +0 -1
  127. package/dist/cjs/perform.js +0 -13
  128. package/dist/cjs/perform.js.map +0 -1
  129. package/dist/dispatch.d.ts +0 -6
  130. package/dist/dispatch.d.ts.map +0 -1
  131. package/dist/element/fill-in.d.ts +0 -12
  132. package/dist/element/fill-in.d.ts.map +0 -1
  133. package/dist/esm/dispatch.js +0 -26
  134. package/dist/esm/dispatch.js.map +0 -1
  135. package/dist/esm/element/fill-in.js +0 -67
  136. package/dist/esm/element/fill-in.js.map +0 -1
  137. package/dist/esm/focused.js +0 -41
  138. package/dist/esm/focused.js.map +0 -1
  139. package/dist/esm/format-table.js.map +0 -1
  140. package/dist/esm/perform.js +0 -9
  141. package/dist/esm/perform.js.map +0 -1
  142. package/dist/focused.d.ts +0 -30
  143. package/dist/focused.d.ts.map +0 -1
  144. package/dist/format-table.d.ts +0 -6
  145. package/dist/format-table.d.ts.map +0 -1
  146. package/dist/perform.d.ts +0 -3
  147. package/dist/perform.d.ts.map +0 -1
  148. package/src/dispatch.ts +0 -24
  149. package/src/element/fill-in.ts +0 -76
  150. package/src/focused.ts +0 -43
  151. package/src/format-table.ts +0 -34
  152. package/src/perform.ts +0 -11
@@ -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 { formatTable } from './format-table';
21
+ import { formatDescription } from './format';
24
22
  import { FilterNotMatchingError } from './errors';
25
- import { interaction, interactionFilter, check, checkFilter, Interaction, ReadonlyInteraction } from './interaction';
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 unsafeSyncResolveParent(options: InteractorOptions<any, any, any>): Element {
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
- ): BaseInteractor<E, FilterParams<E, F>> & ActionMethods<E, A> {
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 description(options);
57
+ return formatDescription(options);
152
58
  },
153
59
 
154
- perform<T>(fn: (element: E) => T): Interaction<T> {
155
- 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
+ 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): ReadonlyInteraction<void> {
159
- return check(`${description(options)} asserts`, () => {
160
- return converge(() => {
161
- fn(resolver(options));
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>): ReadonlyInteraction<void> {
78
+ has(filters: FilterParams<E, F>): AssertionInteraction<E> {
167
79
  return interactor.is(filters);
168
80
  },
169
81
 
170
- is(filters: FilterParams<E, F>): ReadonlyInteraction<void> {
82
+ is(filters: FilterParams<E, F>): AssertionInteraction<E> {
171
83
  let filter = new FilterSet(options.specification, filters);
172
- return check(`${description(options)} matches filters: ${filter.description}`, () => {
173
- return converge(() => {
174
- let element = resolver({...options, filter: getLookupFilterForAssertion(options.filter, filters) });
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(`${description(options)} does not match filters:\n\n${match.formatAsExpectations()}`);
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 interaction(
198
- `${actionDescription} on ${description(options)}`,
199
- () => action(interactor as Interactor<E, FilterParams<E, F>> & ActionMethods<E, A>, ...args)
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 interactionFilter(`${filterName} of ${description(options)}`, async () => {
214
- return applyFilter(filter, resolver(options));
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 BaseInteractor<E, FilterParams<E, F>> & ActionMethods<E, A>;
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 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,12 +1,9 @@
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
- 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 { instantiateBaseInteractor, findElements, unsafeSyncResolveParent } from "./constructor";
4
- import { BaseInteractor, Filters, InteractorConstructor, FilterParams } from "./specification";
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 BaseInteractor<E, FP> {
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
- instantiateBaseInteractor(options, () => element) as (BaseInteractor<GetElement<IC>, Filters<GetFilters<IC>>> & GetActions<IC>), {
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)
@@ -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>;
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 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
+
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
- if(!promise) { promise = this.action(); }
55
- return promise.then(onFulfill, onReject);
117
+ return action().then(onFulfill, onReject);
56
118
  },
57
119
  catch(onReject) {
58
- if(!promise) { promise = this.action(); }
59
- return promise.catch(onReject);
120
+ return action().catch(onReject);
60
121
  },
61
122
  finally(handler) {
62
- if(!promise) { promise = this.action(); }
63
- return promise.finally(handler);
123
+ return action().finally(handler);
64
124
  }
65
125
  }
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) };
126
+ if (apply) {
127
+ return Object.assign(interaction, { apply });
128
+ } else {
129
+ return interaction;
130
+ }
82
131
  }