]>
Commit | Line | Data |
---|---|---|
9f95a23c TL |
1 | import { |
2 | $, | |
3 | $$, | |
4 | browser, | |
5 | by, | |
6 | element, | |
7 | ElementArrayFinder, | |
8 | ElementFinder, | |
9 | protractor | |
10 | } from 'protractor'; | |
11 | ||
12 | const EC = browser.ExpectedConditions; | |
13 | const TIMEOUT = 20000; | |
14 | ||
15 | interface Pages { | |
16 | index: string; | |
17 | } | |
18 | ||
19 | export abstract class PageHelper { | |
20 | pages: Pages; | |
21 | ||
22 | /** | |
23 | * Checks if there are any errors on the browser | |
24 | * | |
25 | * @static | |
26 | * @memberof Helper | |
27 | */ | |
28 | static async checkConsole() { | |
29 | let browserLog = await browser | |
30 | .manage() | |
31 | .logs() | |
32 | .get('browser'); | |
33 | ||
34 | browserLog = browserLog.filter((log) => log.level.value > 900); | |
35 | ||
36 | if (browserLog.length > 0) { | |
37 | console.log('\n log: ' + require('util').inspect(browserLog)); | |
38 | } | |
39 | ||
40 | await expect(browserLog.length).toEqual(0); | |
41 | } | |
42 | ||
43 | /** | |
44 | * Decorator to be used on Helper methods to restrict access to one particular URL. This shall | |
45 | * help developers to prevent and highlight mistakes. It also reduces boilerplate code and by | |
46 | * thus, increases readability. | |
47 | */ | |
48 | static restrictTo(page: string): Function { | |
49 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { | |
50 | const fn: Function = descriptor.value; | |
51 | descriptor.value = function(...args: any) { | |
52 | return browser | |
53 | .getCurrentUrl() | |
54 | .then((url) => | |
55 | url.endsWith(page) | |
56 | ? fn.apply(this, args) | |
57 | : Promise.reject( | |
58 | `Method ${target.constructor.name}::${propertyKey} is supposed to be ` + | |
59 | `run on path "${page}", but was run on URL "${url}"` | |
60 | ) | |
61 | ); | |
62 | }; | |
63 | }; | |
64 | } | |
65 | ||
66 | /** | |
67 | * Get the active breadcrumb item. | |
68 | */ | |
69 | getBreadcrumb(): ElementFinder { | |
70 | return $('.breadcrumb-item.active'); | |
71 | } | |
72 | ||
73 | async getTabText(index: number): Promise<string> { | |
74 | return $$('.nav.nav-tabs li') | |
75 | .get(index) | |
76 | .getText(); | |
77 | } | |
78 | ||
79 | async getTableTotalCount(): Promise<number> { | |
80 | const text = await $$('.datatable-footer-inner .page-count span') | |
81 | .filter(async (e) => (await e.getText()).includes('total')) | |
82 | .first() | |
83 | .getText(); | |
84 | return Number(text.match(/(\d+)\s+total/)[1]); | |
85 | } | |
86 | ||
87 | async getTableSelectedCount(): Promise<number> { | |
88 | const text = await $$('.datatable-footer-inner .page-count span') | |
89 | .filter(async (e) => (await e.getText()).includes('selected')) | |
90 | .first() | |
91 | .getText(); | |
92 | return Number(text.match(/(\d+)\s+selected/)[1]); | |
93 | } | |
94 | ||
95 | async getTableFoundCount(): Promise<number> { | |
96 | const text = await $$('.datatable-footer-inner .page-count span') | |
97 | .filter(async (e) => (await e.getText()).includes('found')) | |
98 | .first() | |
99 | .getText(); | |
100 | return Number(text.match(/(\d+)\s+found/)[1]); | |
101 | } | |
102 | ||
103 | getFirstTableCellWithText(content: string): ElementFinder { | |
104 | return element.all(by.cssContainingText('.datatable-body-cell-label', content)).first(); | |
105 | } | |
106 | ||
107 | getTableRow(content: string) { | |
108 | return element(by.cssContainingText('.datatable-body-row', content)); | |
109 | } | |
110 | ||
111 | getTable(): ElementFinder { | |
112 | return $('.datatable-body'); | |
113 | } | |
114 | ||
115 | async getTabsCount(): Promise<number> { | |
116 | return $$('.nav.nav-tabs li').count(); | |
117 | } | |
118 | ||
119 | /** | |
120 | * Ceph Dashboards' <input type="checkbox"> tag is not visible. Instead of the real checkbox, a | |
121 | * replacement is shown which is supposed to have an adapted style. The replacement checkbox shown | |
122 | * is part of the label and is rendered in the "::before" pseudo element of the label, hence the | |
123 | * label is always clicked when the user clicks the replacement checkbox. | |
124 | * | |
125 | * This method finds corresponding label to the given checkbox and clicks it instead of the (fake) | |
126 | * checkbox, like it is the case with real users. | |
127 | * | |
128 | * Alternatively, the checkbox' label can be passed. | |
129 | * | |
130 | * @param elem The checkbox or corresponding label | |
131 | */ | |
132 | async clickCheckbox(elem: ElementFinder): Promise<void> { | |
133 | const tagName = await elem.getTagName(); | |
134 | let label: ElementFinder = null; // Both types are clickable | |
135 | ||
136 | await this.waitPresence(elem); | |
137 | if (tagName === 'input') { | |
138 | if ((await elem.getAttribute('type')) === 'checkbox') { | |
139 | label = elem.element(by.xpath('..')).$(`label[for="${await elem.getAttribute('id')}"]`); | |
140 | } else { | |
141 | return Promise.reject('element <input> must be of type checkbox'); | |
142 | } | |
143 | } else if (tagName === 'label') { | |
144 | label = elem; | |
145 | } else { | |
146 | return Promise.reject( | |
147 | `element <${tagName}> is not of the correct type. You need to pass a checkbox or label` | |
148 | ); | |
149 | } | |
150 | ||
151 | return this.waitClickableAndClick(label); | |
152 | } | |
153 | ||
154 | /** | |
155 | * Helper method to select an option inside a select element. | |
156 | * This method will also expect that the option was set. | |
157 | * @param option The option text (not value) to be selected. | |
158 | */ | |
159 | async selectOption(selectionName: string, option: string) { | |
160 | await element(by.cssContainingText(`select[name=${selectionName}] option`, option)).click(); | |
161 | return this.expectSelectOption(selectionName, option); | |
162 | } | |
163 | ||
164 | /** | |
165 | * Helper method to expect a set option inside a select element. | |
166 | * @param option The selected option text (not value) that is to | |
167 | * be expected. | |
168 | */ | |
169 | async expectSelectOption(selectionName: string, option: string) { | |
170 | return expect( | |
171 | element(by.css(`select[name=${selectionName}] option:checked`)).getText() | |
172 | ).toContain(option); | |
173 | } | |
174 | ||
175 | /** | |
176 | * Returns the cell with the content given in `content`. Will not return a rejected Promise if the | |
177 | * table cell hasn't been found. It behaves this way to enable to wait for | |
178 | * visibility/invisibility/presence of the returned element. | |
179 | * | |
180 | * It will return a rejected Promise if the result is ambiguous, though. That means if the search | |
181 | * for content has been completed, but more than a single row is shown in the data table. | |
182 | */ | |
183 | async getTableCellByContent(content: string): Promise<ElementFinder> { | |
184 | const searchInput = $('#pool-list > div .search input'); | |
185 | const rowAmountInput = $('#pool-list > div > div > .dataTables_paginate input'); | |
186 | const footer = $('#pool-list > div datatable-footer'); | |
187 | ||
188 | await rowAmountInput.clear(); | |
189 | await rowAmountInput.sendKeys('10'); | |
190 | await searchInput.clear(); | |
191 | await searchInput.sendKeys(content); | |
192 | ||
193 | const count = Number(await footer.getAttribute('ng-reflect-row-count')); | |
194 | if (count !== 0 && count > 1) { | |
195 | return Promise.reject('getTableCellByContent: Result is ambiguous'); | |
196 | } else { | |
197 | return Promise.resolve( | |
198 | element( | |
199 | by.cssContainingText('.datatable-body-cell-label', new RegExp(`^\\s${content}\\s$`)) | |
200 | ) | |
201 | ); | |
202 | } | |
203 | } | |
204 | ||
205 | /** | |
206 | * Used when .clear() does not work on a text box, sends a Ctrl + a, BACKSPACE | |
207 | */ | |
208 | async clearInput(elem: ElementFinder) { | |
209 | const types = ['text', 'number']; | |
210 | if ((await elem.getTagName()) === 'input' && types.includes(await elem.getAttribute('type'))) { | |
211 | return await elem.sendKeys( | |
212 | protractor.Key.chord(protractor.Key.CONTROL, 'a'), | |
213 | protractor.Key.BACK_SPACE | |
214 | ); | |
215 | } else { | |
216 | return Promise.reject(`Element ${elem} does not match the expected criteria.`); | |
217 | } | |
218 | } | |
219 | ||
220 | async navigateTo(page: string = null) { | |
221 | page = page || 'index'; | |
222 | const url = this.pages[page]; | |
223 | await browser.get(url); | |
224 | } | |
225 | ||
226 | async navigateBack() { | |
227 | await browser.navigate().back(); | |
228 | } | |
229 | ||
230 | getDataTables(): ElementArrayFinder { | |
231 | return $$('cd-table'); | |
232 | } | |
233 | ||
234 | /** | |
235 | * Gets column headers of table | |
236 | */ | |
237 | getDataTableHeaders(): ElementArrayFinder { | |
238 | return $$('.datatable-header'); | |
239 | } | |
240 | ||
241 | /** | |
242 | * Grabs striped tables | |
243 | */ | |
244 | getStatusTables(): ElementArrayFinder { | |
245 | return $$('.table.table-striped'); | |
246 | } | |
247 | ||
248 | /** | |
249 | * Grabs legends above tables | |
250 | */ | |
251 | getLegends(): ElementArrayFinder { | |
252 | return $$('legend'); | |
253 | } | |
254 | ||
255 | getToast() { | |
256 | return $('.ngx-toastr'); | |
257 | } | |
258 | ||
259 | async waitPresence(elem: ElementFinder, message?: string) { | |
260 | return browser.wait(EC.presenceOf(elem), TIMEOUT, message); | |
261 | } | |
262 | ||
263 | async waitStaleness(elem: ElementFinder, message?: string) { | |
264 | return browser.wait(EC.stalenessOf(elem), TIMEOUT, message); | |
265 | } | |
266 | ||
267 | /** | |
268 | * This method will wait for the element to be clickable and then click it. | |
269 | */ | |
270 | async waitClickableAndClick(elem: ElementFinder, message?: string) { | |
271 | await browser.wait(EC.elementToBeClickable(elem), TIMEOUT, message); | |
272 | return elem.click(); | |
273 | } | |
274 | ||
275 | async waitVisibility(elem: ElementFinder, message?: string) { | |
276 | return browser.wait(EC.visibilityOf(elem), TIMEOUT, message); | |
277 | } | |
278 | ||
279 | async waitInvisibility(elem: ElementFinder, message?: string) { | |
280 | return browser.wait(EC.invisibilityOf(elem), TIMEOUT, message); | |
281 | } | |
282 | ||
283 | async waitTextToBePresent(elem: ElementFinder, text: string, message?: string) { | |
284 | return browser.wait(EC.textToBePresentInElement(elem, text), TIMEOUT, message); | |
285 | } | |
286 | ||
287 | async waitTextNotPresent(elem: ElementFinder, text: string, message?: string) { | |
288 | return browser.wait(EC.not(EC.textToBePresentInElement(elem, text)), TIMEOUT, message); | |
289 | } | |
290 | ||
801d1391 TL |
291 | async waitFn(func: Function, message?: string, timeout: number = TIMEOUT) { |
292 | return browser.wait(func, timeout, message); | |
9f95a23c TL |
293 | } |
294 | ||
295 | getFirstCell(): ElementFinder { | |
296 | return $$('.datatable-body-cell-label').first(); | |
297 | } | |
298 | ||
299 | /** | |
300 | * This is a generic method to delete table rows. | |
301 | * It will select the first row that contains the provided name and delete it. | |
302 | * After that it will wait until the row is no longer displayed. | |
303 | */ | |
304 | async delete(name: string): Promise<any> { | |
305 | // Selects row | |
306 | await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); | |
307 | ||
308 | // Clicks on table Delete button | |
309 | await $$('.table-actions button.dropdown-toggle') | |
310 | .first() | |
311 | .click(); // open submenu | |
312 | await $('li.delete a').click(); // click on "delete" menu item | |
313 | ||
314 | // Confirms deletion | |
315 | await this.clickCheckbox($('.custom-control-label')); | |
316 | await element(by.cssContainingText('button', 'Delete')).click(); | |
317 | ||
318 | // Waits for item to be removed from table | |
319 | return this.waitStaleness(this.getFirstTableCellWithText(name)); | |
320 | } | |
321 | ||
322 | getTableRows() { | |
323 | return $$('datatable-row-wrapper'); | |
324 | } | |
325 | ||
326 | /** | |
327 | * Uncheck all checked table rows. | |
328 | */ | |
329 | async uncheckAllTableRows() { | |
801d1391 TL |
330 | await $$( |
331 | '.datatable-body-cell-label .datatable-checkbox input[type=checkbox]:checked' | |
332 | ).each((e: ElementFinder) => e.click()); | |
9f95a23c TL |
333 | } |
334 | ||
335 | async filterTable(name: string, option: string) { | |
336 | await this.waitClickableAndClick($('.tc_filter_name > a')); | |
337 | await element(by.cssContainingText(`.tc_filter_name .dropdown-item`, name)).click(); | |
338 | ||
339 | await this.waitClickableAndClick($('.tc_filter_option > a')); | |
340 | await element(by.cssContainingText(`.tc_filter_option .dropdown-item`, option)).click(); | |
341 | } | |
342 | ||
343 | async clearTableSearchInput() { | |
344 | return this.waitClickableAndClick($('cd-table .search button')); | |
345 | } | |
346 | } |