1 | import Ceibo from 'ceibo';
|
2 | import { render, setContext, removeContext } from './-private/context';
|
3 | import { assign, getPageObjectDefinition, isPageObject, storePageObjectDefinition } from './-private/helpers';
|
4 | import { visitable } from './properties/visitable';
|
5 | import dsl from './-private/dsl';
|
6 |
|
7 | //
|
8 | // When running RFC268 tests, we have to play some tricks to support chaining.
|
9 | // RFC268 helpers don't wait for things to settle by defaut, but return a
|
10 | // promise that will resolve when everything settles. So this means
|
11 | //
|
12 | // page.clickOn('.foo');
|
13 | // page.clickOn('.bar');
|
14 | //
|
15 | // will not wait after either of the clicks, whereas
|
16 | //
|
17 | // await page.clickOn('.foo');
|
18 | // await page.clickOn('.bar');
|
19 | //
|
20 | // will wait after each of them. However, to preserve chaining behavior,
|
21 | //
|
22 | // page
|
23 | // .clickOn('.foo')
|
24 | // .clickOn('.bar');
|
25 | //
|
26 | // would need to wait between the clicks. However, if `clickOn()` just returned
|
27 | // `page` this would be impossible because then it would be exactly the same as
|
28 | // the first example, which must not wait between clicks.
|
29 | //
|
30 | // So the solution is to return something other than `page` from,
|
31 | // `page.clickOn('.foo')`, but something that behaves just like `page` except
|
32 | // waits for things to settle before invoking any async methods.
|
33 | //
|
34 | // To accomplish this, when building our Ceibo tree, we build a mirror copy of
|
35 | // it (the "chained tree"). Anytime a chainable method is invoked, instead of
|
36 | // returning the node whose method was invoked, we can return its mirror node in
|
37 | // the chained tree. Then, anytime an async method is invoked on that node
|
38 | // (meaning we are in a chaining scenario), the execution context can recognize
|
39 | // it as a chained node and wait before invoking the target method.
|
40 | //
|
41 |
|
42 | // See https://github.com/san650/ceibo#examples for more info on how Ceibo
|
43 | // builders work.
|
44 |
|
45 | // This builder builds the primary tree
|
46 | function buildObject(node, blueprintKey, blueprint, defaultBuilder) {
|
47 | let definition;
|
48 |
|
49 | // to allow page objects to exist in definitions, we store the definition that
|
50 | // created the page object, allowing us to substitute a page object with its
|
51 | // definition during creation
|
52 | if (isPageObject(blueprint)) {
|
53 | definition = getPageObjectDefinition(blueprint);
|
54 | } else {
|
55 | definition = blueprint;
|
56 | }
|
57 | let blueprintToStore = assign({}, definition);
|
58 | //the _chainedTree is an implementation detail that shouldn't make it into the stored
|
59 | if(blueprintToStore._chainedTree){
|
60 | delete blueprintToStore._chainedTree;
|
61 | }
|
62 | blueprint = assign({}, dsl, definition);
|
63 |
|
64 | const [ instance, blueprintToApply ] = defaultBuilder(node, blueprintKey, blueprint, defaultBuilder);
|
65 |
|
66 | // persist definition once we have an instance
|
67 | storePageObjectDefinition(instance, blueprintToStore);
|
68 |
|
69 | return [ instance, blueprintToApply ];
|
70 | }
|
71 |
|
72 | /**
|
73 | * Creates a new PageObject.
|
74 | *
|
75 | * By default, the resulting PageObject will respond to:
|
76 | *
|
77 | * - **Actions**: click, clickOn, fillIn, select
|
78 | * - **Predicates**: contains, isHidden, isPresent, isVisible
|
79 | * - **Queries**: text
|
80 | *
|
81 | * `definition` can include a key `context`, which is an
|
82 | * optional integration test `this` context.
|
83 | *
|
84 | * If a context is passed, it is used by actions, queries, etc.,
|
85 | * as the `this` in `this.$()`.
|
86 | *
|
87 | * If no context is passed, the global Ember acceptence test
|
88 | * helpers are used.
|
89 | *
|
90 | * @example
|
91 | *
|
92 | * // <div class="title">My title</div>
|
93 | *
|
94 | * import PageObject, { text } from 'ember-cli-page-object';
|
95 | *
|
96 | * const page = PageObject.create({
|
97 | * title: text('.title')
|
98 | * });
|
99 | *
|
100 | * assert.equal(page.title, 'My title');
|
101 | *
|
102 | * @example
|
103 | *
|
104 | * // <div id="my-page">
|
105 | * // My super text
|
106 | * // <button>Press Me</button>
|
107 | * // </div>
|
108 | *
|
109 | * const page = PageObject.create({
|
110 | * scope: '#my-page'
|
111 | * });
|
112 | *
|
113 | * assert.equal(page.text, 'My super text');
|
114 | * assert.ok(page.contains('super'));
|
115 | * assert.ok(page.isPresent);
|
116 | * assert.ok(page.isVisible);
|
117 | * assert.notOk(page.isHidden);
|
118 | * assert.equal(page.value, 'my input value');
|
119 | *
|
120 | * // clicks div#my-page
|
121 | * page.click();
|
122 | *
|
123 | * // clicks button
|
124 | * page.clickOn('Press Me');
|
125 | *
|
126 | * // fills an input
|
127 | * page.fillIn('name', 'John Doe');
|
128 | *
|
129 | * // selects an option
|
130 | * page.select('country', 'Uruguay');
|
131 | *
|
132 | * @example Defining path
|
133 | *
|
134 | * const usersPage = PageObject.create('/users');
|
135 | *
|
136 | * // visits user page
|
137 | * usersPage.visit();
|
138 | *
|
139 | * const userTasksPage = PageObject.create('/users/tasks', {
|
140 | * tasks: collection({
|
141 | * itemScope: '.tasks li',
|
142 | * item: {}
|
143 | * });
|
144 | * });
|
145 | *
|
146 | * // get user's tasks
|
147 | * userTasksPage.visit();
|
148 | * userTasksPage.tasks().count
|
149 | *
|
150 | * @public
|
151 | *
|
152 | * @param {Object} definition - PageObject definition
|
153 | * @param {Object} [definition.context] - A test's `this` context
|
154 | * @param {Object} options - [private] Ceibo options. Do not use!
|
155 | * @return {PageObject}
|
156 | */
|
157 | export function create(definitionOrUrl, definitionOrOptions, optionsOrNothing) {
|
158 | let definition;
|
159 | let url;
|
160 | let options;
|
161 |
|
162 | if (typeof (definitionOrUrl) === 'string') {
|
163 | url = definitionOrUrl;
|
164 | definition = definitionOrOptions || {};
|
165 | options = optionsOrNothing || {};
|
166 | } else {
|
167 | url = false;
|
168 | definition = definitionOrUrl || {};
|
169 | options = definitionOrOptions || {};
|
170 | }
|
171 |
|
172 | let { context } = definition;
|
173 | // in the instance where the definition is a page object, we must use the stored definition directly
|
174 | // or else we will fire off the Ceibo created getters which will error
|
175 | definition = assign({}, isPageObject(definition) ? getPageObjectDefinition(definition) : definition);
|
176 | delete definition.context;
|
177 |
|
178 | if (url) {
|
179 | definition.visit = visitable(url);
|
180 | }
|
181 |
|
182 | // Build the chained tree
|
183 | let chainedBuilder = {
|
184 | object: buildObject
|
185 | };
|
186 | let chainedTree = Ceibo.create(definition, assign({ builder: chainedBuilder }, options));
|
187 |
|
188 | // Attach it to the root in the definition of the primary tree
|
189 | definition._chainedTree = {
|
190 | isDescriptor: true,
|
191 |
|
192 | get() {
|
193 | return chainedTree;
|
194 | }
|
195 | };
|
196 |
|
197 | // Build the primary tree
|
198 | let builder = {
|
199 | object: buildObject
|
200 | };
|
201 |
|
202 | let page = Ceibo.create(definition, assign({ builder }, options));
|
203 |
|
204 | if (page) {
|
205 | page.render = render;
|
206 | page.setContext = setContext;
|
207 | page.removeContext = removeContext;
|
208 |
|
209 | if (typeof context !== 'undefined') {
|
210 | page.setContext(context);
|
211 | }
|
212 | }
|
213 |
|
214 | return page;
|
215 | }
|