e4f6fd603a12bbca7e5e3fb31cb5cdea338841a0
[arvados-workbench2.git] / cypress / support / commands.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 // ***********************************************
6 // This example commands.js shows you how to
7 // create various custom commands and overwrite
8 // existing commands.
9 //
10 // For more comprehensive examples of custom
11 // commands please read more here:
12 // https://on.cypress.io/custom-commands
13 // ***********************************************
14 //
15 //
16 // -- This is a parent command --
17 // Cypress.Commands.add("login", (email, password) => { ... })
18 //
19 //
20 // -- This is a child command --
21 // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
22 //
23 //
24 // -- This is a dual command --
25 // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
26 //
27 //
28 // -- This will overwrite an existing command --
29 // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
30
31 import { extractFilesData } from "services/collection-service/collection-service-files-response";
32
33 const controllerURL = Cypress.env('controller_url');
34 const systemToken = Cypress.env('system_token');
35 let createdResources = [];
36
37 // Clean up on a 'before' hook to allow post-mortem analysis on individual tests.
38 beforeEach(function () {
39     if (createdResources.length === 0) {
40         return;
41     }
42     cy.log(`Cleaning ${createdResources.length} previously created resource(s)`);
43     createdResources.forEach(function({suffix, uuid}) {
44         // Don't fail when a resource isn't already there, some objects may have
45         // been removed, directly or indirectly, from the test that created them.
46         cy.deleteResource(systemToken, suffix, uuid, false);
47     });
48     createdResources = [];
49 });
50
51 Cypress.Commands.add(
52     "doRequest", (method = 'GET', path = '', data = null, qs = null,
53         token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
54     return cy.request({
55         method: method,
56         url: `${controllerURL.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`,
57         body: data,
58         qs: auth ? qs : Object.assign({ api_token: token }, qs),
59         auth: auth ? { bearer: `${token}` } : undefined,
60         followRedirect: followRedirect,
61         failOnStatusCode: failOnStatusCode
62     });
63 });
64
65 Cypress.Commands.add(
66     "doKeepRequest", (method = 'GET', path = '', data = null, qs = null,
67         token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
68     return cy.doRequest('GET', '/arvados/v1/config', null, null).then(({body: config}) => {
69         return cy.request({
70             method: method,
71             url: `${config.Services.WebDAVDownload.ExternalURL.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`,
72             body: data,
73             qs: auth ? qs : Object.assign({ api_token: token }, qs),
74             auth: auth ? { bearer: `${token}` } : undefined,
75             followRedirect: followRedirect,
76             failOnStatusCode: failOnStatusCode
77         });
78     });
79 });
80
81 Cypress.Commands.add(
82     "getUser", (username, first_name = '', last_name = '', is_admin = false, is_active = true) => {
83         // Create user if not already created
84         return cy.doRequest('POST', '/auth/controller/callback', {
85             auth_info: JSON.stringify({
86                 email: `${username}@example.local`,
87                 username: username,
88                 first_name: first_name,
89                 last_name: last_name,
90                 alternate_emails: []
91             }),
92             return_to: ',https://example.local'
93         }, null, systemToken, true, false) // Don't follow redirects so we can catch the token
94         .its('headers.location').as('location')
95         // Get its token and set the account up as admin and/or active
96         .then(function () {
97             this.userToken = this.location.split("=")[1]
98             assert.isString(this.userToken)
99             return cy.doRequest('GET', '/arvados/v1/users', null, {
100                 filters: `[["username", "=", "${username}"]]`
101             })
102             .its('body.items.0').as('aUser')
103             .then(function () {
104                 cy.doRequest('PUT', `/arvados/v1/users/${this.aUser.uuid}`, {
105                     user: {
106                         is_admin: is_admin,
107                         is_active: is_active
108                     }
109                 })
110                 .its('body').as('theUser')
111                 .then(function () {
112                     cy.doRequest('GET', '/arvados/v1/api_clients', null, {
113                         filters: `[["is_trusted", "=", false]]`,
114                         order: `["created_at desc"]`
115                     })
116                     .its('body.items').as('apiClients')
117                     .then(function () {
118                         if (this.apiClients.length > 0) {
119                             cy.doRequest('PUT', `/arvados/v1/api_clients/${this.apiClients[0].uuid}`, {
120                                 api_client: {
121                                     is_trusted: true
122                                 }
123                             })
124                             .its('body').as('updatedApiClient')
125                             .then(function() {
126                                 assert(this.updatedApiClient.is_trusted);
127                             })
128                         }
129                     })
130                     .then(function () {
131                         return { user: this.theUser, token: this.userToken };
132                     })
133                 })
134             })
135         })
136     }
137 )
138
139 Cypress.Commands.add(
140     "createLink", (token, data) => {
141         return cy.createResource(token, 'links', {
142             link: JSON.stringify(data)
143         })
144     }
145 )
146
147 Cypress.Commands.add(
148     "createGroup", (token, data) => {
149         return cy.createResource(token, 'groups', {
150             group: JSON.stringify(data),
151             ensure_unique_name: true
152         })
153     }
154 )
155
156 Cypress.Commands.add(
157     "trashGroup", (token, uuid) => {
158         return cy.deleteResource(token, 'groups', uuid);
159     }
160 )
161
162
163 Cypress.Commands.add(
164     "createWorkflow", (token, data) => {
165         return cy.createResource(token, 'workflows', {
166             workflow: JSON.stringify(data),
167             ensure_unique_name: true
168         })
169     }
170 )
171
172 Cypress.Commands.add(
173     "createCollection", (token, data) => {
174         return cy.createResource(token, 'collections', {
175             collection: JSON.stringify(data),
176             ensure_unique_name: true
177         })
178     }
179 )
180
181 Cypress.Commands.add(
182     "getCollection", (token, uuid) => {
183         return cy.getResource(token, 'collections', uuid)
184     }
185 )
186
187 Cypress.Commands.add(
188     "updateCollection", (token, uuid, data) => {
189         return cy.updateResource(token, 'collections', uuid, {
190             collection: JSON.stringify(data)
191         })
192     }
193 )
194
195 Cypress.Commands.add(
196     "getContainer", (token, uuid) => {
197         return cy.getResource(token, 'containers', uuid)
198     }
199 )
200
201 Cypress.Commands.add(
202     "updateContainer", (token, uuid, data) => {
203         return cy.updateResource(token, 'containers', uuid, {
204             container: JSON.stringify(data)
205         })
206     }
207 )
208
209 Cypress.Commands.add(
210     "getContainerRequest", (token, uuid) => {
211         return cy.getResource(token, 'container_requests', uuid)
212     }
213 )
214
215 Cypress.Commands.add(
216     'createContainerRequest', (token, data) => {
217         return cy.createResource(token, 'container_requests', {
218             container_request: JSON.stringify(data),
219             ensure_unique_name: true
220         })
221     }
222 )
223
224 Cypress.Commands.add(
225     "updateContainerRequest", (token, uuid, data) => {
226         return cy.updateResource(token, 'container_requests', uuid, {
227             container_request: JSON.stringify(data)
228         })
229     }
230 )
231
232 /**
233  * Requires an admin token for log_uuid modification to succeed
234  */
235 Cypress.Commands.add(
236     "appendLog", (token, crUuid, fileName, lines = []) => (
237         cy.getContainerRequest(token, crUuid).then((containerRequest) => {
238             if (containerRequest.log_uuid) {
239                 cy.listContainerRequestLogs(token, crUuid).then((logFiles) => {
240                     if (logFiles.find((file) => (file.name === fileName))) {
241                         // File exists, fetch and append
242                         return cy.doKeepRequest(
243                                 "GET",
244                                 `c=${containerRequest.log_uuid}/${fileName}`,
245                                 null,
246                                 null,
247                                 token
248                             )
249                             .then(({ body: contents }) => cy.doKeepRequest(
250                                 "PUT",
251                                 `c=${containerRequest.log_uuid}/${fileName}`,
252                                 contents.split("\n").concat(lines).join("\n"),
253                                 null,
254                                 token
255                             ));
256                     } else {
257                         // File not exists, put new file
258                         cy.doKeepRequest(
259                             "PUT",
260                             `c=${containerRequest.log_uuid}/${fileName}`,
261                             lines.join("\n"),
262                             null,
263                             token
264                         )
265                     }
266                 });
267                 // Fetch current log contents and append new line
268                 // let newLines = [...lines];
269                 // return cy.doKeepRequest('GET', `c=${containerRequest.log_uuid}/${fileName}`, null, null, token)
270                 //     .then(({body: contents}) => {
271                 //         newLines = [contents.split('\n'), ...newLines];
272                 //     })
273                 //     .then(() => (
274                 //         cy.doKeepRequest('PUT', `c=${containerRequest.log_uuid}/${fileName}`, newLines.join('\n'), null, token)
275                 //     ));
276             } else {
277                 // Create log collection
278                 return cy.createCollection(token, {
279                     name: `Test log collection ${Math.floor(Math.random() * 999999)}`,
280                     owner_uuid: containerRequest.owner_uuid,
281                     manifest_text: ""
282                 }).then((collection) => (
283                     // Update CR log_uuid to fake log collection
284                     cy.updateContainerRequest(token, containerRequest.uuid, {
285                         log_uuid: collection.uuid,
286                     }).then(() => (
287                         // Put new log file with contents into fake log collection
288                         cy.doKeepRequest('PUT', `c=${collection.uuid}/${fileName}`, lines.join('\n'), null, token)
289                     ))
290                 ));
291             }
292         })
293     )
294 )
295
296 Cypress.Commands.add(
297     "listContainerRequestLogs", (token, crUuid) => (
298         cy.getContainerRequest(token, crUuid).then((containerRequest) => (
299             cy.doKeepRequest('PROPFIND', `c=${containerRequest.log_uuid}`, null, null, token)
300                 .then(({body: data}) => {
301                     return extractFilesData(new DOMParser().parseFromString(data, "text/xml"));
302                 })
303         ))
304     )
305 );
306
307 Cypress.Commands.add(
308     "createVirtualMachine", (token, data) => {
309         return cy.createResource(token, 'virtual_machines', {
310             virtual_machine: JSON.stringify(data),
311             ensure_unique_name: true
312         })
313     }
314 )
315
316 Cypress.Commands.add(
317     "getResource", (token, suffix, uuid) => {
318         return cy.doRequest('GET', `/arvados/v1/${suffix}/${uuid}`, null, {}, token)
319             .its('body')
320             .then(function (resource) {
321                 return resource;
322             })
323     }
324 )
325
326 Cypress.Commands.add(
327     "createResource", (token, suffix, data) => {
328         return cy.doRequest('POST', '/arvados/v1/' + suffix, data, null, token, true)
329             .its('body')
330             .then(function (resource) {
331                 createdResources.push({suffix, uuid: resource.uuid});
332                 return resource;
333             })
334     }
335 )
336
337 Cypress.Commands.add(
338     "deleteResource", (token, suffix, uuid, failOnStatusCode = true) => {
339         return cy.doRequest('DELETE', '/arvados/v1/' + suffix + '/' + uuid, null, null, token, false, true, failOnStatusCode)
340             .its('body')
341             .then(function (resource) {
342                 return resource;
343             })
344     }
345 )
346
347 Cypress.Commands.add(
348     "updateResource", (token, suffix, uuid, data) => {
349         return cy.doRequest('PATCH', '/arvados/v1/' + suffix + '/' + uuid, data, null, token, true)
350             .its('body')
351             .then(function (resource) {
352                 return resource;
353             })
354     }
355 )
356
357 Cypress.Commands.add(
358     "loginAs", (user) => {
359         cy.clearCookies()
360         cy.clearLocalStorage()
361         cy.visit(`/token/?api_token=${user.token}`);
362         cy.url({timeout: 10000}).should('contain', '/projects/');
363         cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)');
364         cy.get('div#root').should('not.contain', 'Your account is inactive');
365     }
366 )
367
368 Cypress.Commands.add(
369     "testEditProjectOrCollection", (container, oldName, newName, newDescription, isProject = true) => {
370         cy.get(container).contains(oldName).rightclick();
371         cy.get('[data-cy=context-menu]').contains(isProject ? 'Edit project' : 'Edit collection').click();
372         cy.get('[data-cy=form-dialog]').within(() => {
373             cy.get('input[name=name]').clear().type(newName);
374             cy.get(isProject ? 'div[contenteditable=true]' : 'input[name=description]').clear().type(newDescription);
375             cy.get('[data-cy=form-submit-btn]').click();
376         });
377
378         cy.get(container).contains(newName).rightclick();
379         cy.get('[data-cy=context-menu]').contains(isProject ? 'Edit project' : 'Edit collection').click();
380         cy.get('[data-cy=form-dialog]').within(() => {
381             cy.get('input[name=name]').should('have.value', newName);
382
383             if (isProject) {
384                 cy.get('span[data-text=true]').contains(newDescription);
385             } else {
386                 cy.get('input[name=description]').should('have.value', newDescription);
387             }
388
389             cy.get('[data-cy=form-cancel-btn]').click();
390         });
391     }
392 )
393
394 Cypress.Commands.add(
395     "doSearch", (searchTerm) => {
396         cy.get('[data-cy=searchbar-input-field]').type(`{selectall}${searchTerm}{enter}`);
397     }
398 )
399
400 Cypress.Commands.add(
401     "goToPath", (path) => {
402         return cy.window().its('appHistory').invoke('push', path);
403     }
404 )
405
406 Cypress.Commands.add('getAll', (...elements) => {
407     const promise = cy.wrap([], { log: false })
408
409     for (let element of elements) {
410         promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got])))
411     }
412
413     return promise
414 })
415
416 Cypress.Commands.add('shareWith', (srcUserToken, targetUserUUID, itemUUID, permission = 'can_write') => {
417     cy.createLink(srcUserToken, {
418         name: permission,
419         link_class: 'permission',
420         head_uuid: itemUUID,
421         tail_uuid: targetUserUUID
422     });
423 })
424
425 Cypress.Commands.add('addToFavorites', (userToken, userUUID, itemUUID) => {
426     cy.createLink(userToken, {
427         head_uuid: itemUUID,
428         link_class: 'star',
429         name: '',
430         owner_uuid: userUUID,
431         tail_uuid: userUUID,
432     });
433 })
434
435 Cypress.Commands.add('createProject', ({
436     owningUser,
437     targetUser,
438     projectName,
439     canWrite,
440     addToFavorites
441 }) => {
442     const writePermission = canWrite ? 'can_write' : 'can_read';
443
444     cy.createGroup(owningUser.token, {
445         name: `${projectName} ${Math.floor(Math.random() * 999999)}`,
446         group_class: 'project',
447     }).as(`${projectName}`).then((project) => {
448         if (targetUser && targetUser !== owningUser) {
449             cy.shareWith(owningUser.token, targetUser.user.uuid, project.uuid, writePermission);
450         }
451         if (addToFavorites) {
452             const user = targetUser ? targetUser : owningUser;
453             cy.addToFavorites(user.token, user.user.uuid, project.uuid);
454         }
455     });
456 });
457
458 Cypress.Commands.add(
459     'upload',
460     {
461         prevSubject: 'element',
462     },
463     (subject, file, fileName, binaryMode = true) => {
464         cy.window().then(window => {
465             const blob = binaryMode
466                 ? b64toBlob(file, '', 512)
467                 : new Blob([file], {type: 'text/plain'});
468             const testFile = new window.File([blob], fileName);
469
470             cy.wrap(subject).trigger('drop', {
471                 dataTransfer: { files: [testFile] },
472             });
473         })
474     }
475 )
476
477 function b64toBlob(b64Data, contentType = '', sliceSize = 512) {
478     const byteCharacters = atob(b64Data)
479     const byteArrays = []
480
481     for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
482         const slice = byteCharacters.slice(offset, offset + sliceSize);
483
484         const byteNumbers = new Array(slice.length);
485         for (let i = 0; i < slice.length; i++) {
486             byteNumbers[i] = slice.charCodeAt(i);
487         }
488
489         const byteArray = new Uint8Array(byteNumbers);
490
491         byteArrays.push(byteArray);
492     }
493
494     const blob = new Blob(byteArrays, { type: contentType });
495     return blob
496 }
497
498 // From https://github.com/cypress-io/cypress/issues/7306#issuecomment-1076451070=
499 // This command requires the async package (https://www.npmjs.com/package/async)
500 Cypress.Commands.add('waitForDom', () => {
501     cy.window().then({
502         // Don't timeout before waitForDom finishes
503         timeout: 10000
504     }, win => {
505       let timeElapsed = 0;
506
507       cy.log("Waiting for DOM mutations to complete");
508
509       return new Cypress.Promise((resolve) => {
510         // set the required variables
511         let async = require("async");
512         let observerConfig = { attributes: true, childList: true, subtree: true };
513         let items = Array.apply(null, { length: 50 }).map(Number.call, Number);
514         win.mutationCount = 0;
515         win.previousMutationCount = null;
516
517         // create an observer instance
518         let observer = new win.MutationObserver((mutations) => {
519           mutations.forEach((mutation) => {
520             // Only record "attributes" type mutations that are not a "class" mutation.
521             // If the mutation is not an "attributes" type, then we always record it.
522             if (mutation.type === 'attributes' && mutation.attributeName !== 'class') {
523               win.mutationCount += 1;
524             } else if (mutation.type !== 'attributes') {
525               win.mutationCount += 1;
526             }
527           });
528
529           // initialize the previousMutationCount
530           if (win.previousMutationCount == null) win.previousMutationCount = 0;
531         });
532
533         // watch the document body for the specified mutations
534         observer.observe(win.document.body, observerConfig);
535
536         // check the DOM for mutations up to 50 times for a maximum time of 5 seconds
537         async.eachSeries(items, function iteratee(item, callback) {
538           // keep track of the elapsed time so we can log it at the end of the command
539           timeElapsed = timeElapsed + 100;
540
541           // make each iteration of the loop 100ms apart
542           setTimeout(() => {
543             if (win.mutationCount === win.previousMutationCount) {
544               // pass an argument to the async callback to exit the loop
545               return callback('Resolved - DOM changes complete.');
546             } else if (win.previousMutationCount != null) {
547               // only set the previous count if the observer has checked the DOM at least once
548               win.previousMutationCount = win.mutationCount;
549               return callback();
550             } else if (win.mutationCount === 0 && win.previousMutationCount == null && item === 4) {
551               // this is an early exit in case nothing is changing in the DOM. That way we only
552               // wait 500ms instead of the full 5 seconds when no DOM changes are occurring.
553               return callback('Resolved - Exiting early since no DOM changes were detected.');
554             } else {
555               // proceed to the next iteration
556               return callback();
557             }
558           }, 100);
559         }, function done() {
560           // Log the total wait time so users can see it
561           cy.log(`DOM mutations ${timeElapsed >= 5000 ? "did not complete" : "completed"} in ${timeElapsed} ms`);
562
563           // disconnect the observer and resolve the promise
564           observer.disconnect();
565           resolve();
566         });
567       });
568     });
569   });