]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts
d/control: depend on python3-yaml for ceph-mgr
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / e2e / page-helper.po.ts
CommitLineData
9f95a23c
TL
1import {
2 $,
3 $$,
4 browser,
5 by,
6 element,
7 ElementArrayFinder,
8 ElementFinder,
9 protractor
10} from 'protractor';
11
12const EC = browser.ExpectedConditions;
13const TIMEOUT = 20000;
14
15interface Pages {
16 index: string;
17}
18
19export 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}