UNPKG

6.27 kBJavaScriptView Raw
1import Ceibo from 'ceibo';
2import { render, setContext, removeContext } from './-private/context';
3import { assign, getPageObjectDefinition, isPageObject, storePageObjectDefinition } from './-private/helpers';
4import { visitable } from './properties/visitable';
5import 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
46function 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 */
157export 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}