1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 // ***********************************************
6 // This example commands.js shows you how to
7 // create various custom commands and overwrite
10 // For more comprehensive examples of custom
11 // commands please read more here:
12 // https://on.cypress.io/custom-commands
13 // ***********************************************
16 // -- This is a parent command --
17 // Cypress.Commands.add("login", (email, password) => { ... })
20 // -- This is a child command --
21 // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
24 // -- This is a dual command --
25 // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
28 // -- This will overwrite an existing command --
29 // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
31 import 'cypress-wait-until';
32 import { extractFilesData } from "services/collection-service/collection-service-files-response";
34 const controllerURL = Cypress.env("controller_url");
35 const systemToken = Cypress.env("system_token");
36 let createdResources = [];
38 const containerLogFolderPrefix = "log for container ";
40 // Clean up anything that was created. You can temporarily add
41 // 'return' to the top if you need the resources to hang around to
42 // debug a specific test.
43 afterEach(function () {
44 if (createdResources.length === 0) {
47 cy.log(`Cleaning ${createdResources.length} previously created resource(s).`);
48 // delete them in FIFO order because later created resources may
49 // be linked to the earlier ones.
50 createdResources.reverse().forEach(function ({ suffix, uuid }) {
51 // Don't fail when a resource isn't already there, some objects may have
52 // been removed, directly or indirectly, from the test that created them.
53 cy.deleteResource(systemToken, suffix, uuid, false);
55 createdResources = [];
60 (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
63 url: `${controllerURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`,
65 qs: auth ? qs : Object.assign({ api_token: token }, qs),
66 auth: auth ? { bearer: `${token}` } : undefined,
67 followRedirect: followRedirect,
68 failOnStatusCode: failOnStatusCode,
75 (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
76 return cy.doRequest("GET", "/arvados/v1/config", null, null).then(({ body: config }) => {
79 url: `${config.Services.WebDAVDownload.ExternalURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`,
81 qs: auth ? qs : Object.assign({ api_token: token }, qs),
82 auth: auth ? { bearer: `${token}` } : undefined,
83 followRedirect: followRedirect,
84 failOnStatusCode: failOnStatusCode,
90 Cypress.Commands.add("getUser", (username, first_name = "", last_name = "", is_admin = false, is_active = true) => {
91 // Create user if not already created
96 "/auth/controller/callback",
98 auth_info: JSON.stringify({
99 email: `${username}@example.local`,
101 first_name: first_name,
102 last_name: last_name,
103 alternate_emails: [],
105 return_to: ",https://controller.api.client.invalid",
111 ) // Don't follow redirects so we can catch the token
112 .its("headers.location")
114 // Get its token and set the account up as admin and/or active
116 this.userToken = this.location.split("=")[1];
117 assert.isString(this.userToken);
119 .doRequest("GET", "/arvados/v1/users", null, {
120 filters: `[["username", "=", "${username}"]]`,
125 cy.doRequest("PUT", `/arvados/v1/users/${this.aUser.uuid}`, {
128 is_active: is_active,
134 return { user: this.theUser, token: this.userToken };
141 Cypress.Commands.add("createLink", (token, data) => {
142 return cy.createResource(token, "links", {
143 link: JSON.stringify(data),
147 Cypress.Commands.add("createGroup", (token, data) => {
148 return cy.createResource(token, "groups", {
149 group: JSON.stringify(data),
150 ensure_unique_name: true,
154 Cypress.Commands.add("trashGroup", (token, uuid) => {
155 return cy.deleteResource(token, "groups", uuid);
158 Cypress.Commands.add("createWorkflow", (token, data) => {
159 return cy.createResource(token, "workflows", {
160 workflow: JSON.stringify(data),
161 ensure_unique_name: true,
165 Cypress.Commands.add("createCollection", (token, data, keep = false) => {
166 return cy.createResource(token, "collections", {
167 collection: JSON.stringify(data),
168 ensure_unique_name: true,
172 Cypress.Commands.add("getCollection", (token, uuid) => {
173 return cy.getResource(token, "collections", uuid);
176 Cypress.Commands.add("updateCollection", (token, uuid, data) => {
177 return cy.updateResource(token, "collections", uuid, {
178 collection: JSON.stringify(data),
182 Cypress.Commands.add("collectionReplaceFiles", (token, uuid, data) => {
183 return cy.updateResource(token, "collections", uuid, {
185 preserve_version: true,
187 replace_files: JSON.stringify(data),
191 Cypress.Commands.add("getContainer", (token, uuid) => {
192 return cy.getResource(token, "containers", uuid);
195 Cypress.Commands.add("updateContainer", (token, uuid, data) => {
196 return cy.updateResource(token, "containers", uuid, {
197 container: JSON.stringify(data),
201 Cypress.Commands.add("getContainerRequest", (token, uuid) => {
202 return cy.getResource(token, "container_requests", uuid);
205 Cypress.Commands.add("createContainerRequest", (token, data) => {
206 return cy.createResource(token, "container_requests", {
207 container_request: JSON.stringify(data),
208 ensure_unique_name: true,
212 Cypress.Commands.add("updateContainerRequest", (token, uuid, data) => {
213 return cy.updateResource(token, "container_requests", uuid, {
214 container_request: JSON.stringify(data),
219 * Requires an admin token for log_uuid modification to succeed
221 Cypress.Commands.add("appendLog", (token, crUuid, fileName, lines = []) =>
222 cy.getContainerRequest(token, crUuid).then(containerRequest => {
223 if (containerRequest.log_uuid) {
224 cy.listContainerRequestLogs(token, crUuid).then(logFiles => {
225 const filePath = `${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`;
226 if (logFiles.find(file => file.name === fileName)) {
227 // File exists, fetch and append
229 .doWebDAVRequest("GET", `c=${filePath}`, null, null, token)
230 .then(({ body: contents }) =>
231 cy.doWebDAVRequest("PUT", `c=${filePath}`, contents.split("\n").concat(lines).join("\n"), null, token)
234 // File not exists, put new file
235 cy.doWebDAVRequest("PUT", `c=${filePath}`, lines.join("\n"), null, token);
239 // Create log collection
241 .createCollection(token, {
242 name: `Test log collection ${Math.floor(Math.random() * 999999)}`,
243 owner_uuid: containerRequest.owner_uuid,
246 .then(collection => {
247 // Update CR log_uuid to fake log collection
248 cy.updateContainerRequest(token, containerRequest.uuid, {
249 log_uuid: collection.uuid,
251 // Create empty directory for container uuid
253 .collectionReplaceFiles(token, collection.uuid, {
254 [`/${containerLogFolderPrefix}${containerRequest.container_uuid}`]: "d41d8cd98f00b204e9800998ecf8427e+0",
257 // Put new log file with contents into fake log collection
260 `c=${collection.uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`,
272 Cypress.Commands.add("listContainerRequestLogs", (token, crUuid) =>
273 cy.getContainerRequest(token, crUuid).then(containerRequest =>
277 `c=${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}`,
282 .then(({ body: data }) => {
283 return extractFilesData(new DOMParser().parseFromString(data, "text/xml"));
288 Cypress.Commands.add("createVirtualMachine", (token, data) => {
289 return cy.createResource(token, "virtual_machines", {
290 virtual_machine: JSON.stringify(data),
291 ensure_unique_name: true,
295 Cypress.Commands.add("getResource", (token, suffix, uuid) => {
297 .doRequest("GET", `/arvados/v1/${suffix}/${uuid}`, null, {}, token)
299 .then(function (resource) {
304 Cypress.Commands.add("createResource", (token, suffix, data, keep = false) => {
306 .doRequest("POST", "/arvados/v1/" + suffix, data, null, token, true)
308 .then(function (resource) {
310 createdResources.push({ suffix, uuid: resource.uuid });
317 Cypress.Commands.add("deleteResource", (token, suffix, uuid, failOnStatusCode = true) => {
319 .doRequest("DELETE", "/arvados/v1/" + suffix + "/" + uuid, null, null, token, false, true, failOnStatusCode)
321 .then(function (resource) {
326 Cypress.Commands.add("updateResource", (token, suffix, uuid, data) => {
328 .doRequest("PATCH", "/arvados/v1/" + suffix + "/" + uuid, data, null, token, true)
330 .then(function (resource) {
335 Cypress.Commands.add("loginAs", (user, preserveLocalStorage = false) => {
336 // This shouldn't be necessary unless we need to call loginAs multiple times
339 if(preserveLocalStorage === false) {
340 cy.clearAllLocalStorage();
341 cy.clearAllSessionStorage();
343 cy.visit(`/token/?api_token=${user.token}`);
344 // Use waitUntil to avoid permafail race conditions with window.location being undefined
345 cy.waitUntil(() => cy.window().then(win =>
346 win?.location?.href &&
347 win.location.href.includes("/projects/")
348 ), { timeout: 15000 });
349 // Wait for page to settle before getting elements
351 cy.get("div#root").should("contain", "Arvados Workbench (zzzzz)");
352 cy.get("div#root").should("not.contain", "Your account is inactive");
355 Cypress.Commands.add("testEditProjectOrCollection", (container, oldName, newName, newDescription, isProject = true) => {
356 cy.get(container).contains(oldName).rightclick();
357 cy.get("[data-cy=context-menu]")
358 .contains(isProject ? "Edit project" : "Edit collection")
360 cy.get("[data-cy=form-dialog]").within(() => {
361 cy.get("input[name=name]").clear().type(newName);
362 cy.get(isProject ? "div[contenteditable=true]" : "input[name=description]")
364 .type(newDescription);
365 cy.get("[data-cy=form-submit-btn]").click();
368 cy.get(container).contains(newName).rightclick();
369 cy.get("[data-cy=context-menu]")
370 .contains(isProject ? "Edit project" : "Edit collection")
372 cy.get("[data-cy=form-dialog]").within(() => {
373 cy.get("input[name=name]").should("have.value", newName);
376 cy.get("span[data-text=true]").contains(newDescription);
378 cy.get("input[name=description]").should("have.value", newDescription);
381 cy.get("[data-cy=form-cancel-btn]").click();
385 Cypress.Commands.add("doSearch", searchTerm => {
386 cy.get("[data-cy=searchbar-input-field]").type(`{selectall}${searchTerm}{enter}`);
389 Cypress.Commands.add("goToPath", path => {
390 return cy.visit(path);
393 Cypress.Commands.add("getAll", (...elements) => {
394 const promise = cy.wrap([], { log: false });
396 for (let element of elements) {
397 promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got])));
403 Cypress.Commands.add("shareWith", (srcUserToken, targetUserUUID, itemUUID, permission = "can_write") => {
404 cy.createLink(srcUserToken, {
406 link_class: "permission",
408 tail_uuid: targetUserUUID,
412 Cypress.Commands.add("addToFavorites", (userToken, userUUID, itemUUID) => {
413 cy.createLink(userToken, {
417 owner_uuid: userUUID,
422 Cypress.Commands.add("createProject", ({ owningUser, targetUser, projectName, canWrite, addToFavorites }) => {
423 const writePermission = canWrite ? "can_write" : "can_read";
425 cy.createGroup(owningUser.token, {
426 name: `${projectName} ${Math.floor(Math.random() * 999999)}`,
427 group_class: "project",
429 .as(`${projectName}`)
431 if (targetUser && targetUser !== owningUser) {
432 cy.shareWith(owningUser.token, targetUser.user.uuid, project.uuid, writePermission);
434 if (addToFavorites) {
435 const user = targetUser ? targetUser : owningUser;
436 cy.addToFavorites(user.token, user.user.uuid, project.uuid);
441 Cypress.Commands.add(
444 prevSubject: "element",
446 (subject, file, fileName, binaryMode = true) => {
447 cy.window().then(window => {
448 const blob = binaryMode ? b64toBlob(file, "", 512) : new Blob([file], { type: "text/plain" });
449 const testFile = new window.File([blob], fileName);
451 cy.wrap(subject).trigger("drop", {
452 dataTransfer: { files: [testFile] },
458 function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
459 const byteCharacters = atob(b64Data);
460 const byteArrays = [];
462 for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
463 const slice = byteCharacters.slice(offset, offset + sliceSize);
465 const byteNumbers = new Array(slice.length);
466 for (let i = 0; i < slice.length; i++) {
467 byteNumbers[i] = slice.charCodeAt(i);
470 const byteArray = new Uint8Array(byteNumbers);
472 byteArrays.push(byteArray);
475 const blob = new Blob(byteArrays, { type: contentType });
479 // From https://github.com/cypress-io/cypress/issues/7306#issuecomment-1076451070=
480 // This command requires the async package (https://www.npmjs.com/package/async)
481 Cypress.Commands.add("waitForDom", () => {
482 cy.window({ timeout: 10000 }).then(
484 // Don't timeout before waitForDom finishes
490 cy.log("Waiting for DOM mutations to complete");
492 return new Cypress.Promise(resolve => {
493 // set the required variables
494 let async = require("async");
495 let observerConfig = { attributes: true, childList: true, subtree: true };
496 let items = Array.apply(null, { length: 50 }).map(Number.call, Number);
497 win.mutationCount = 0;
498 win.previousMutationCount = null;
500 // create an observer instance
501 let observer = new win.MutationObserver(mutations => {
502 mutations.forEach(mutation => {
503 // Only record "attributes" type mutations that are not a "class" mutation.
504 // If the mutation is not an "attributes" type, then we always record it.
505 if (mutation.type === "attributes" && mutation.attributeName !== "class") {
506 win.mutationCount += 1;
507 } else if (mutation.type !== "attributes") {
508 win.mutationCount += 1;
512 // initialize the previousMutationCount
513 if (win.previousMutationCount == null) win.previousMutationCount = 0;
516 // watch the document body for the specified mutations
517 observer.observe(win.document.body, observerConfig);
519 // check the DOM for mutations up to 50 times for a maximum time of 5 seconds
522 function iteratee(item, callback) {
523 // keep track of the elapsed time so we can log it at the end of the command
524 timeElapsed = timeElapsed + 100;
526 // make each iteration of the loop 100ms apart
528 if (win.mutationCount === win.previousMutationCount) {
529 // pass an argument to the async callback to exit the loop
530 return callback("Resolved - DOM changes complete.");
531 } else if (win.previousMutationCount != null) {
532 // only set the previous count if the observer has checked the DOM at least once
533 win.previousMutationCount = win.mutationCount;
535 } else if (win.mutationCount === 0 && win.previousMutationCount == null && item === 4) {
536 // this is an early exit in case nothing is changing in the DOM. That way we only
537 // wait 500ms instead of the full 5 seconds when no DOM changes are occurring.
538 return callback("Resolved - Exiting early since no DOM changes were detected.");
540 // proceed to the next iteration
546 // Log the total wait time so users can see it
547 cy.log(`DOM mutations ${timeElapsed >= 5000 ? "did not complete" : "completed"} in ${timeElapsed} ms`);
549 // disconnect the observer and resolve the promise
550 observer.disconnect();
559 Cypress.Commands.add('waitForLocalStorage', (key, options = {}) => {
560 const timeout = options.timeout || 10000;
561 const interval = options.interval || 100;
563 cy.log(`Waiting for localStorage key: ${key}`)
565 const checkLocalStorage = () => {
566 return new Cypress.Promise((resolve, reject) => {
567 const startTime = Date.now();
569 const check = () => {
570 const value = localStorage.getItem(key);
572 if (value !== null) {
574 } else if (Date.now() - startTime > timeout) {
575 reject(new Error(`Timed out waiting for localStorage key: ${key}`));
577 setTimeout(check, interval);
585 return cy.wrap(checkLocalStorage());
588 //pauses test execution until the localStorage key changes
589 Cypress.Commands.add('waitForLocalStorageUpdate', (key, timeout = 10000) => {
590 const checkInterval = 200; // Interval to check the localStorage value
591 let previousValue = localStorage.getItem(key);
593 return new Cypress.Promise((resolve, reject) => {
594 const checkValue = () => {
595 const currentValue = localStorage.getItem(key);
596 if (currentValue !== previousValue) {
597 resolve(currentValue);
598 } else if (Date.now() - startTime >= timeout) {
599 reject(new Error(`Timed out waiting for localStorage key "${key}" to change`));
601 setTimeout(checkValue, checkInterval);
605 const startTime = Date.now();
610 Cypress.Commands.add("setupDockerImage", (image_name) => {
611 // Create a collection that will be used as a docker image for the tests.
615 cy.getUser("admin", "Admin", "User", true, true)
618 adminUser = this.adminUser;
621 cy.getUser('activeuser', 'Active', 'User', false, true)
622 .as('activeUser').then(function () {
623 activeUser = this.activeUser;
626 cy.getAll('@activeUser', '@adminUser').then(([activeUser, adminUser]) => {
627 cy.createCollection(adminUser.token, {
628 name: "docker_image",
630 ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n",
633 .then(function (dockerImage) {
634 // Give read permissions to the active user on the docker image.
635 cy.createLink(adminUser.token, {
636 link_class: "permission",
638 tail_uuid: activeUser.user.uuid,
639 head_uuid: dockerImage.uuid,
641 .as("dockerImagePermission")
643 // Set-up docker image collection tags
644 cy.createLink(activeUser.token, {
645 link_class: "docker_image_repo+tag",
647 head_uuid: dockerImage.uuid,
648 }).as("dockerImageRepoTag");
649 cy.createLink(activeUser.token, {
650 link_class: "docker_image_hash",
651 name: "sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678",
652 head_uuid: dockerImage.uuid,
653 }).as("dockerImageHash");
657 return cy.getAll("@dockerImage", "@dockerImageRepoTag", "@dockerImageHash", "@dockerImagePermission").then(function ([dockerImage]) {