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