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