1 | import Ceibo from 'ceibo';
|
2 | import deprecate from './-private/deprecate';
|
3 | import { render, setContext, removeContext } from './-private/context';
|
4 | import { assign, getPageObjectDefinition, isPageObject, storePageObjectDefinition } from './-private/helpers';
|
5 | import { visitable } from './properties/visitable';
|
6 | import dsl from './-private/dsl';
|
7 |
|
8 | function 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
|
57 | function 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 | */
|
189 | export 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 | }
|