Skip to content

Commit f377aec

Browse files
authored
Merge pull request #42 from NullVoxPopuli/helper-utilities
Add some helpful utilities
2 parents b23244e + b722d90 commit f377aec

File tree

7 files changed

+213
-52
lines changed

7 files changed

+213
-52
lines changed

packages/fractal-page-object/API.md

Lines changed: 93 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,34 @@
22

33
### Table of Contents
44

5-
- [PageObject][1]
6-
- [Parameters][2]
7-
- [Examples][3]
8-
- [element][4]
9-
- [elements][5]
10-
- [selector][6]
11-
- [Parameters][7]
12-
- [Examples][8]
13-
- [globalSelector][9]
14-
- [Parameters][10]
15-
- [Examples][11]
16-
- [setRoot][12]
17-
- [Parameters][13]
5+
* [PageObject][1]
6+
* [Parameters][2]
7+
* [Examples][3]
8+
* [element][4]
9+
* [elements][5]
10+
* [selector][6]
11+
* [Parameters][7]
12+
* [Examples][8]
13+
* [globalSelector][9]
14+
* [Parameters][10]
15+
* [Examples][11]
16+
* [setRoot][12]
17+
* [Parameters][13]
18+
* [assertExists][14]
19+
* [Parameters][15]
20+
* [Examples][16]
21+
* [getDescription][17]
22+
* [Parameters][18]
1823

1924
## PageObject
2025

26+
**Extends ArrayStub**
27+
2128
This class implements all the basic page object functionality, and all page
2229
objects must inherit from it. It can host [selector][6] and
2330
[globalSelector][9] fields, and will properly instantiate them as nested
2431
[PageObject][1]s when accessed. Each page object represents a DOM query
25-
that matches zero or more [Element][14]s.
32+
that matches zero or more [Element][19]s.
2633

2734
[PageObject][1]s exist in a tree where each [PageObject][1]'s elements
2835
are descendants of its parent's elements. The root of the tree is a top-level
@@ -53,13 +60,13 @@ they are constructed, but is evaluated and re-evaluated each time a property
5360
that depends on it is accessed.
5461

5562
[PageObject][1]s expose an API for interacting with their matching
56-
elements that comprises [PageObject#element][15],
57-
[PageObject#elements][16], and an [Array][17] API that exposes the page
58-
object's matching [Element][14]s wrapped in indexed [PageObject][1]s.
63+
elements that comprises [PageObject#element][20],
64+
[PageObject#elements][21], and an [Array][22] API that exposes the page
65+
object's matching [Element][19]s wrapped in indexed [PageObject][1]s.
5966
The index operator will return an indexed [PageObject][1] that may or may
6067
not match an element (similar to how you can index off the end of a native
6168
array and get `undefined`), while various array iteration methods like
62-
[PageObject#map][18] generate a range of [PageObject][1]s that reflect
69+
[PageObject#map][23] generate a range of [PageObject][1]s that reflect
6370
only the indices that actually match an element.
6471

6572
Descendant [PageObject][1]s are defined by subclassing [PageObject][1]
@@ -70,10 +77,10 @@ will match the root element (the body element or whatever was )
7077

7178
### Parameters
7279

73-
- `selector` **[string][19]?** the selector to use for this page object's query (optional, default `''`)
74-
- `parent` (for internal use only) (optional, default `null`)
75-
- `index` (for internal use only) (optional, default `null`)
76-
- `rootElement` (for internal use only) (optional, default `null`)
80+
* `selector` **[string][24]?** the selector to use for this page object's query (optional, default `''`)
81+
* `parent` (for internal use only) (optional, default `null`)
82+
* `index` (for internal use only) (optional, default `null`)
83+
* `rootElement` (for internal use only) (optional, default `null`)
7784

7885
### Examples
7986

@@ -110,14 +117,14 @@ matching this page object's query if this page object does not have an
110117
index, or the `index`th matching DOM element if it does have an index
111118
specified.
112119

113-
Type: ([Element][20] | null)
120+
Type: ([Element][25] | null)
114121

115122
### elements
116123

117124
This page object's list of matching DOM elements. If this page object has
118125
an index, this property will always have a length of 0 or 1.
119126

120-
Type: [Array][21]<[Element][20]>
127+
Type: [Array][26]<[Element][25]>
121128

122129
## selector
123130

@@ -128,8 +135,8 @@ properties and functions.
128135

129136
### Parameters
130137

131-
- `selector` **[string][19]** the selector relative to the parent node
132-
- `Class` **[Function][22]&lt;[PageObject][23]>?** optional [PageObject][1] subclass that
138+
* `selector` **[string][24]** the selector relative to the parent node
139+
* `Class` **[Function][27]<[PageObject][28]>?** optional [PageObject][1] subclass that
133140
can be used to extend the functionality of this page object
134141

135142
### Examples
@@ -147,7 +154,7 @@ page.list.elements; // document.body.querySelectorAll('.list')
147154
page.list.items.elements; // document.body.querySelectorAll('.list li')
148155
```
149156

150-
Returns **[PageObject][23]** a [PageObject][1] or [PageObject][1] subclass
157+
Returns **[PageObject][28]** a [PageObject][1] or [PageObject][1] subclass
151158
instance
152159

153160
## globalSelector
@@ -157,7 +164,7 @@ parent page object. Useful for cases like popovers and dropdowns, where the
157164
UI control is logically inside a given component, but all or part of it
158165
renders elsewhere in the DOM, such as directly under the body.
159166
[globalSelector][9] accepts a selector and optional custom class like
160-
[selector()][24], but the queries of the page objects it generates will be
167+
[selector()][29], but the queries of the page objects it generates will be
161168
executed from the root (`document.body` or whatever was passed to
162169
[setRoot][12]) rather than the parent page object's elements.
163170

@@ -169,9 +176,9 @@ generates.
169176

170177
### Parameters
171178

172-
- `args` **...any**
173-
- `selector` **[string][19]** the selector
174-
- `Class` **[Function][22]&lt;[PageObject][23]>** optional [PageObject][1] subclass that
179+
* `args` **...any**
180+
* `selector` **[string][24]** the selector
181+
* `Class` **[Function][27]<[PageObject][28]>** optional [PageObject][1] subclass that
175182
can be used to extend the functionality of this page object
176183

177184
### Examples
@@ -214,7 +221,7 @@ page.listItems[0].popover; // document.body.querySelectorAll('.popover')
214221
page.listItems[0].popover.icon; // document.body.querySelectorAll('.popover .icon')
215222
```
216223

217-
Returns **[PageObject][23]** a [PageObject][1] or [PageObject][1] subclass
224+
Returns **[PageObject][28]** a [PageObject][1] or [PageObject][1] subclass
218225
instance
219226

220227
## setRoot
@@ -225,9 +232,41 @@ element is `document.body`.
225232

226233
### Parameters
227234

228-
- `element` **([Element][20] \| [Function][22])** the root element or a function that will
235+
* `element` **([Element][25] | [Function][27])** the root element or a function that will
229236
return it
230237

238+
## assertExists
239+
240+
Useful for providing clarity to consumers of page-objects
241+
to provide additional context so "can't access property on undefined"
242+
errors do not public up to the consumer.
243+
244+
In typescript, this is also useful for type-narrowing so that
245+
you can pass on the element to other utilities.
246+
247+
### Parameters
248+
249+
* `msg` **[string][24]** a descriptor for what it could mean when the element doesn't exist
250+
* `pageObject` **[PageObject][28]** the page object
251+
252+
### Examples
253+
254+
```javascript
255+
let page = new Page();
256+
257+
assertExists('is the element on the page?', page);
258+
259+
await click(page.element);
260+
```
261+
262+
## getDescription
263+
264+
Utility to get the fully resolved selector path of a [PageObject][1]
265+
266+
### Parameters
267+
268+
* `pageObject`
269+
231270
[1]: #pageobject
232271

233272
[2]: #parameters
@@ -254,24 +293,34 @@ element is `document.body`.
254293

255294
[13]: #parameters-3
256295

257-
[14]: https://developer.mozilla.org/docs/Web/API/Element
296+
[14]: #assertexists
297+
298+
[15]: #parameters-4
299+
300+
[16]: #examples-3
301+
302+
[17]: #getdescription
303+
304+
[18]: #parameters-5
305+
306+
[19]: https://developer.mozilla.org/docs/Web/API/Element
258307

259-
[15]: #pageobjectelement
308+
[20]: #pageobjectelement
260309

261-
[16]: #pageobjectelements
310+
[21]: #pageobjectelements
262311

263-
[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
312+
[22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
264313

265-
[18]: PageObject#map
314+
[23]: PageObject#map
266315

267-
[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
316+
[24]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
268317

269-
[20]: https://developer.mozilla.org/docs/Web/API/Element
318+
[25]: https://developer.mozilla.org/docs/Web/API/Element
270319

271-
[21]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
320+
[26]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
272321

273-
[22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
322+
[27]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
274323

275-
[23]: #pageobject
324+
[28]: #pageobject
276325

277-
[24]: selector()
326+
[29]: selector\(\)

packages/fractal-page-object/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"build": "rollup -c",
2020
"changelog": "lerna-changelog",
2121
"docs": "yarn docs:build && yarn documentation build doc-build/index.js --document-exported --config documentation.yml -f md -o API.md && yarn docs:clean",
22-
"docs:build": "tsc --noEmit false --rootDir src/ --outDir doc-build/",
22+
"docs:build": "tsc --noEmit false --skipLibCheck true --rootDir src/ --outDir doc-build/",
2323
"docs:clean": "rm -rf src/*.js src/**/*.js doc-build",
2424
"lint": "eslint .",
2525
"prepublish": "yarn build",

packages/fractal-page-object/src/-private/array-stub.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
/* istanbul ignore file */
22

3-
/**
4-
* Helper type for array prototype stubbing
5-
*
6-
* @see {@link ArrayStub#map} etc.
7-
*/
8-
type WithElement<T> = T & { element: Element };
3+
import type { WithElement } from './types';
94

105
/**
116
* Base class for {@link PageObject} that contains stub implementations of a

packages/fractal-page-object/src/-private/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ export const CLONE_WITH_INDEX = Symbol('withIndex');
99
export type PageObjectConstructor<T extends PageObject> = new (
1010
...args: ConstructorParameters<typeof PageObject>
1111
) => T | PageObject;
12+
13+
/**
14+
* Helper type for a {@link PageObject}s and subclasses that are known to match an element
15+
*
16+
* @see {@link ArrayStub#map} etc.
17+
*/
18+
export type WithElement<T> = T & { element: Element };
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, afterEach, test, expect } from '@jest/globals';
2+
import { PageObject, selector, assertExists, getDescription } from '../index';
3+
import { resetRoot } from '../-private/root';
4+
5+
describe('utils', () => {
6+
afterEach(() => resetRoot());
7+
8+
describe('assertExists', () => {
9+
test('element exists', () => {
10+
document.body.innerHTML = '<div>boop</div>';
11+
12+
class Page extends PageObject {}
13+
let page = new Page('div');
14+
15+
try {
16+
assertExists('test', page);
17+
} catch {
18+
expect('This should not error').toEqual(false);
19+
}
20+
21+
expect(true).toEqual(true);
22+
});
23+
24+
test('element missing', () => {
25+
document.body.innerHTML = '';
26+
27+
class Page extends PageObject {}
28+
let page = new Page('div');
29+
30+
expect(() => {
31+
assertExists('test', page);
32+
}).toThrow(/Tried selector `div`/);
33+
});
34+
35+
test('selector shown is deep', () => {
36+
document.body.innerHTML = '';
37+
38+
class Page extends PageObject {
39+
nested = selector('button');
40+
}
41+
let page = new Page('div');
42+
43+
expect(() => {
44+
assertExists('test', page.nested);
45+
}).toThrow(/Tried selector `div button`/);
46+
});
47+
});
48+
49+
describe('getDescription', () => {
50+
test('it works', () => {
51+
class Page extends PageObject {
52+
thing = selector(
53+
'.thing',
54+
class extends PageObject {
55+
subthing = selector('.subthing');
56+
}
57+
);
58+
}
59+
let page = new Page();
60+
61+
expect(getDescription(page.thing)).toEqual('.thing');
62+
expect(getDescription(page.thing.subthing)).toEqual('.thing .subthing');
63+
expect(getDescription(page.thing[1].subthing[0])).toEqual(
64+
'.thing[1] .subthing[0]'
65+
);
66+
});
67+
});
68+
});
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export { setRoot } from './-private/root';
2-
export type { PageObjectConstructor } from './-private/types';
2+
export type { PageObjectConstructor, WithElement } from './-private/types';
33

44
export { default as PageObject } from './page-object';
55

66
export { default as selector } from './selector';
77
export { default as globalSelector } from './global-selector';
8+
9+
export { assertExists, getDescription } from './utils';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { DOM_QUERY, WithElement } from './-private/types';
2+
3+
import type { default as PageObject } from './page-object';
4+
5+
/**
6+
* Useful for providing clarity to consumers of page-objects
7+
* to provide additional context so "can't access property on undefined"
8+
* errors do not public up to the consumer.
9+
*
10+
* In typescript, this is also useful for type-narrowing so that
11+
* you can pass on the element to other utilities.
12+
*
13+
* @example
14+
*
15+
* let page = new Page();
16+
*
17+
* assertExists('is the element on the page?', page);
18+
*
19+
* await click(page.element);
20+
*
21+
* @param {string} msg a descriptor for what it could mean when the element doesn't exist
22+
* @param {PageObject} pageObject the page object
23+
*/
24+
export function assertExists(
25+
msg: string,
26+
pageObject: PageObject
27+
): asserts pageObject is WithElement<PageObject> {
28+
if (!pageObject.element) {
29+
throw new Error(
30+
`${msg} >> Tried selector \`${getDescription(pageObject)}\``
31+
);
32+
}
33+
}
34+
35+
/**
36+
* Utility to get the fully resolved selector path of a {@link PageObject}
37+
*/
38+
export function getDescription(pageObject: PageObject): string {
39+
return pageObject[DOM_QUERY].selectorArray.toString();
40+
}

0 commit comments

Comments
 (0)