@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.
Files changed (121) hide show
  1. package/dist/cjs/constructor.js +55 -126
  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.map +1 -1
  8. package/dist/cjs/inspector.js +2 -1
  9. package/dist/cjs/inspector.js.map +1 -1
  10. package/dist/cjs/interaction.js +54 -35
  11. package/dist/cjs/interaction.js.map +1 -1
  12. package/dist/cjs/match.js +17 -7
  13. package/dist/cjs/match.js.map +1 -1
  14. package/dist/cjs/matcher.js +13 -1
  15. package/dist/cjs/matcher.js.map +1 -1
  16. package/dist/cjs/matchers/and.js +3 -0
  17. package/dist/cjs/matchers/and.js.map +1 -1
  18. package/dist/cjs/matchers/every.js +3 -0
  19. package/dist/cjs/matchers/every.js.map +1 -1
  20. package/dist/cjs/matchers/including.js +4 -0
  21. package/dist/cjs/matchers/including.js.map +1 -1
  22. package/dist/cjs/matchers/matching.js +4 -0
  23. package/dist/cjs/matchers/matching.js.map +1 -1
  24. package/dist/cjs/matchers/not.js +3 -0
  25. package/dist/cjs/matchers/not.js.map +1 -1
  26. package/dist/cjs/matchers/or.js +3 -0
  27. package/dist/cjs/matchers/or.js.map +1 -1
  28. package/dist/cjs/matchers/some.js +3 -0
  29. package/dist/cjs/matchers/some.js.map +1 -1
  30. package/dist/cjs/resolvers.js +98 -0
  31. package/dist/cjs/resolvers.js.map +1 -0
  32. package/dist/cjs/serialize.js +48 -0
  33. package/dist/cjs/serialize.js.map +1 -0
  34. package/dist/constructor.d.ts +2 -8
  35. package/dist/constructor.d.ts.map +1 -1
  36. package/dist/converge.d.ts +2 -1
  37. package/dist/converge.d.ts.map +1 -1
  38. package/dist/esm/constructor.js +52 -117
  39. package/dist/esm/constructor.js.map +1 -1
  40. package/dist/esm/converge.js +3 -5
  41. package/dist/esm/converge.js.map +1 -1
  42. package/dist/esm/{format-table.js → format.js} +19 -1
  43. package/dist/esm/format.js.map +1 -0
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/inspector.js +2 -1
  46. package/dist/esm/inspector.js.map +1 -1
  47. package/dist/esm/interaction.js +51 -29
  48. package/dist/esm/interaction.js.map +1 -1
  49. package/dist/esm/match.js +17 -7
  50. package/dist/esm/match.js.map +1 -1
  51. package/dist/esm/matcher.js +11 -0
  52. package/dist/esm/matcher.js.map +1 -1
  53. package/dist/esm/matchers/and.js +4 -1
  54. package/dist/esm/matchers/and.js.map +1 -1
  55. package/dist/esm/matchers/every.js +4 -1
  56. package/dist/esm/matchers/every.js.map +1 -1
  57. package/dist/esm/matchers/including.js +4 -0
  58. package/dist/esm/matchers/including.js.map +1 -1
  59. package/dist/esm/matchers/matching.js +4 -0
  60. package/dist/esm/matchers/matching.js.map +1 -1
  61. package/dist/esm/matchers/not.js +4 -1
  62. package/dist/esm/matchers/not.js.map +1 -1
  63. package/dist/esm/matchers/or.js +4 -1
  64. package/dist/esm/matchers/or.js.map +1 -1
  65. package/dist/esm/matchers/some.js +4 -1
  66. package/dist/esm/matchers/some.js.map +1 -1
  67. package/dist/esm/resolvers.js +88 -0
  68. package/dist/esm/resolvers.js.map +1 -0
  69. package/dist/esm/serialize.js +43 -0
  70. package/dist/esm/serialize.js.map +1 -0
  71. package/dist/format.d.ts +10 -0
  72. package/dist/format.d.ts.map +1 -0
  73. package/dist/index.d.ts +4 -2
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/inspector.d.ts.map +1 -1
  76. package/dist/interaction.d.ts +38 -10
  77. package/dist/interaction.d.ts.map +1 -1
  78. package/dist/match.d.ts +5 -5
  79. package/dist/match.d.ts.map +1 -1
  80. package/dist/matcher.d.ts +2 -0
  81. package/dist/matcher.d.ts.map +1 -1
  82. package/dist/matchers/and.d.ts.map +1 -1
  83. package/dist/matchers/every.d.ts.map +1 -1
  84. package/dist/matchers/including.d.ts.map +1 -1
  85. package/dist/matchers/matching.d.ts.map +1 -1
  86. package/dist/matchers/not.d.ts.map +1 -1
  87. package/dist/matchers/or.d.ts.map +1 -1
  88. package/dist/matchers/some.d.ts.map +1 -1
  89. package/dist/resolvers.d.ts +9 -0
  90. package/dist/resolvers.d.ts.map +1 -0
  91. package/dist/serialize.d.ts +13 -0
  92. package/dist/serialize.d.ts.map +1 -0
  93. package/dist/specification.d.ts +19 -17
  94. package/dist/specification.d.ts.map +1 -1
  95. package/dist/tsconfig.build.tsbuildinfo +1 -1
  96. package/package.json +3 -2
  97. package/src/constructor.ts +63 -138
  98. package/src/converge.ts +3 -6
  99. package/src/format.ts +58 -0
  100. package/src/index.ts +4 -2
  101. package/src/inspector.ts +2 -1
  102. package/src/interaction.ts +96 -36
  103. package/src/match.ts +23 -15
  104. package/src/matcher.ts +11 -0
  105. package/src/matchers/and.ts +4 -1
  106. package/src/matchers/every.ts +4 -1
  107. package/src/matchers/including.ts +4 -1
  108. package/src/matchers/matching.ts +4 -1
  109. package/src/matchers/not.ts +4 -1
  110. package/src/matchers/or.ts +4 -1
  111. package/src/matchers/some.ts +4 -1
  112. package/src/resolvers.ts +100 -0
  113. package/src/serialize.ts +54 -0
  114. package/src/specification.ts +21 -19
  115. package/CHANGELOG.md +0 -27
  116. package/dist/cjs/format-table.js +0 -30
  117. package/dist/cjs/format-table.js.map +0 -1
  118. package/dist/esm/format-table.js.map +0 -1
  119. package/dist/format-table.d.ts +0 -6
  120. package/dist/format-table.d.ts.map +0 -1
  121. 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, 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>;
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 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(): 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
- if(!promise) { promise = this.action(); }
55
- return promise.then(onFulfill, onReject);
128
+ return action().then(onFulfill, onReject);
56
129
  },
57
130
  catch(onReject) {
58
- if(!promise) { promise = this.action(); }
59
- return promise.catch(onReject);
131
+ return action().catch(onReject);
60
132
  },
61
133
  finally(handler) {
62
- if(!promise) { promise = this.action(); }
63
- return promise.finally(handler);
134
+ return action().finally(handler);
64
135
  }
65
136
  }
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) };
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
- this.matches = (this.matchLocator ? this.matchLocator.matches : true) && this.matchFilter.matches;
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
- this.matches = applyMatcher(this.expected, this.actual);
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
- this.matches = this.items.every((match) => match.matches)
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
- if(this.filter.specification.filters && this.filter.specification.filters[this.key]) {
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(definition) === 'function') {
129
- this.actual = definition(this.element) as T;
137
+ if (typeof definition === 'function') {
138
+ return definition(this.element) as T;
130
139
  } else {
131
- this.actual = definition.apply(this.element) as T;
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
+ }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
+ }
@@ -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
+ }