UNPKG

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