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 { extractFilesData } from "services/collection-service/collection-service-files-response";
33 const controllerURL = Cypress.env("controller_url");
34 const systemToken = Cypress.env("system_token");
35 let createdResources = [];
37 const containerLogFolderPrefix = "log for container ";
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) {
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);
52 createdResources = [];
57 (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
60 url: `${controllerURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`,
62 qs: auth ? qs : Object.assign({ api_token: token }, qs),
63 auth: auth ? { bearer: `${token}` } : undefined,
64 followRedirect: followRedirect,
65 failOnStatusCode: failOnStatusCode,
70 Cypress.Commands.add("getUser", (username, first_name = "", last_name = "", is_admin = false, is_active = true) => {
71 // Create user if not already created
76 "/auth/controller/callback",
78 auth_info: JSON.stringify({
79 email: `${username}@example.local`,
81 first_name: first_name,
85 return_to: ",https://example.local",
91 ) // Don't follow redirects so we can catch the token
92 .its("headers.location")
94 // Get its token and set the account up as admin and/or active
96 this.userToken = this.location.split("=")[1];
97 assert.isString(this.userToken);
99 .doRequest("GET", "/arvados/v1/users", null, {
100 filters: `[["username", "=", "${username}"]]`,
105 cy.doRequest("PUT", `/arvados/v1/users/${this.aUser.uuid}`, {
108 is_active: is_active,
114 cy.doRequest("GET", "/arvados/v1/api_clients", null, {
115 filters: `[["is_trusted", "=", false]]`,
116 order: `["created_at desc"]`,
121 if (this.apiClients.length > 0) {
122 cy.doRequest("PUT", `/arvados/v1/api_clients/${this.apiClients[0].uuid}`, {
128 .as("updatedApiClient")
130 assert(this.updatedApiClient.is_trusted);
135 return { user: this.theUser, token: this.userToken };
143 Cypress.Commands.add("createLink", (token, data) => {
144 return cy.createResource(token, "links", {
145 link: JSON.stringify(data),
149 Cypress.Commands.add(
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 }) => {
155 url: `${config.Services.WebDAVDownload.ExternalURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`,
157 qs: auth ? qs : Object.assign({ api_token: token }, qs),
158 auth: auth ? { bearer: `${token}` } : undefined,
159 followRedirect: followRedirect,
160 failOnStatusCode: failOnStatusCode,
166 Cypress.Commands.add("getUser", (username, first_name = "", last_name = "", is_admin = false, is_active = true) => {
167 // Create user if not already created
172 "/auth/controller/callback",
174 auth_info: JSON.stringify({
175 email: `${username}@example.local`,
177 first_name: first_name,
178 last_name: last_name,
179 alternate_emails: [],
181 return_to: ",https://example.local",
187 ) // Don't follow redirects so we can catch the token
188 .its("headers.location")
190 // Get its token and set the account up as admin and/or active
193 this.userToken = this.location.split("=")[1];
194 assert.isString(this.userToken);
196 .doRequest("GET", "/arvados/v1/users", null, {
197 filters: `[["username", "=", "${username}"]]`,
202 cy.doRequest("PUT", `/arvados/v1/users/${this.aUser.uuid}`, {
205 is_active: is_active,
207 }).as("lastLogRecord");
210 cy.getAll("@lastLogRecord").then(function () {
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,
224 Cypress.Commands.add("getResource", (token, suffix, uuid) => {
226 .doRequest("GET", `/arvados/v1/${suffix}/${uuid}`, null, {}, token)
228 .then(function (resource) {
233 Cypress.Commands.add("createResource", (token, suffix, data) => {
235 .doRequest("POST", "/arvados/v1/" + suffix, data, null, token, true)
237 .then(function (resource) {
238 createdResources.push({ suffix, uuid: resource.uuid });
243 Cypress.Commands.add("deleteResource", (token, suffix, uuid, failOnStatusCode = true) => {
245 .doRequest("DELETE", "/arvados/v1/" + suffix + "/" + uuid, null, null, token, false, true, failOnStatusCode)
247 .then(function (resource) {
252 Cypress.Commands.add("updateResource", (token, suffix, uuid, data) => {
254 .doRequest("PATCH", "/arvados/v1/" + suffix + "/" + uuid, data, null, token, true)
256 .then(function (resource) {
261 Cypress.Commands.add("createCollection", (token, data) => {
262 return cy.createResource(token, "collections", {
263 collection: JSON.stringify(data),
264 ensure_unique_name: true,
268 Cypress.Commands.add("getCollection", (token, uuid) => {
269 return cy.getResource(token, "collections", uuid);
272 Cypress.Commands.add("updateCollection", (token, uuid, data) => {
273 return cy.updateResource(token, "collections", uuid, {
274 collection: JSON.stringify(data),
278 Cypress.Commands.add("collectionReplaceFiles", (token, uuid, data) => {
279 return cy.updateResource(token, "collections", uuid, {
281 preserve_version: true,
283 replace_files: JSON.stringify(data),
287 Cypress.Commands.add("getContainer", (token, uuid) => {
288 return cy.getResource(token, "containers", uuid);
291 Cypress.Commands.add("updateContainer", (token, uuid, data) => {
292 return cy.updateResource(token, "containers", uuid, {
293 container: JSON.stringify(data),
297 Cypress.Commands.add("getContainerRequest", (token, uuid) => {
298 return cy.getResource(token, "container_requests", uuid);
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,
308 Cypress.Commands.add("updateContainerRequest", (token, uuid, data) => {
309 return cy.updateResource(token, "container_requests", uuid, {
310 container_request: JSON.stringify(data),
315 * Requires an admin token for log_uuid modification to succeed
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
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)
330 // File not exists, put new file
331 cy.doWebDAVRequest("PUT", `c=${filePath}`, lines.join("\n"), null, token);
335 // Create log collection
337 .createCollection(token, {
338 name: `Test log collection ${Math.floor(Math.random() * 999999)}`,
339 owner_uuid: containerRequest.owner_uuid,
342 .then(collection => {
343 // Update CR log_uuid to fake log collection
344 cy.updateContainerRequest(token, containerRequest.uuid, {
345 log_uuid: collection.uuid,
347 // Create empty directory for container uuid
349 .collectionReplaceFiles(token, collection.uuid, {
350 [`/${containerLogFolderPrefix}${containerRequest.container_uuid}`]: "d41d8cd98f00b204e9800998ecf8427e+0",
353 // Put new log file with contents into fake log collection
356 `c=${collection.uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`,
368 Cypress.Commands.add("listContainerRequestLogs", (token, crUuid) =>
369 cy.getContainerRequest(token, crUuid).then(containerRequest =>
373 `c=${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}`,
378 .then(({ body: data }) => {
379 return extractFilesData(new DOMParser().parseFromString(data, "text/xml"));
384 cy.get("[data-cy=form-cancel-btn]").click();
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();
391 Cypress.Commands.add("goToPath", path => {
392 return cy.window().its("appHistory").invoke("push", path);
395 Cypress.Commands.add("getAll", (...elements) => {
396 const promise = cy.wrap([], { log: false });
398 for (let element of elements) {
399 promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got])));
405 Cypress.Commands.add("shareWith", (srcUserToken, targetUserUUID, itemUUID, permission = "can_write") => {
406 cy.createLink(srcUserToken, {
408 link_class: "permission",
410 tail_uuid: targetUserUUID,
414 Cypress.Commands.add("addToFavorites", (userToken, userUUID, itemUUID) => {
415 cy.createLink(userToken, {
419 owner_uuid: userUUID,
424 Cypress.Commands.add("createProject", ({ owningUser, targetUser, projectName, canWrite, addToFavorites }) => {
425 const writePermission = canWrite ? "can_write" : "can_read";
427 cy.createGroup(owningUser.token, {
428 name: `${projectName} ${Math.floor(Math.random() * 999999)}`,
429 group_class: "project",
431 .as(`${projectName}`)
433 if (targetUser && targetUser !== owningUser) {
434 cy.shareWith(owningUser.token, targetUser.user.uuid, project.uuid, writePermission);
436 if (addToFavorites) {
437 const user = targetUser ? targetUser : owningUser;
438 cy.addToFavorites(user.token, user.user.uuid, project.uuid);
443 Cypress.Commands.add(
446 prevSubject: "element",
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);
453 cy.wrap(subject).trigger("drop", {
454 dataTransfer: { files: [testFile] },
460 function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
461 const byteCharacters = atob(b64Data);
462 const byteArrays = [];
464 for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
465 const slice = byteCharacters.slice(offset, offset + sliceSize);
467 const byteNumbers = new Array(slice.length);
468 for (let i = 0; i < slice.length; i++) {
469 byteNumbers[i] = slice.charCodeAt(i);
472 const byteArray = new Uint8Array(byteNumbers);
474 byteArrays.push(byteArray);
477 const blob = new Blob(byteArrays, { type: contentType });
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", () => {
486 // Don't timeout before waitForDom finishes
492 cy.log("Waiting for DOM mutations to complete");
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;
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;
514 // initialize the previousMutationCount
515 if (win.previousMutationCount == null) win.previousMutationCount = 0;
518 // watch the document body for the specified mutations
519 observer.observe(win.document.body, observerConfig);
521 // check the DOM for mutations up to 50 times for a maximum time of 5 seconds
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;
528 // make each iteration of the loop 100ms apart
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;
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.");
542 // proceed to the next iteration
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`);
551 // disconnect the observer and resolve the promise
552 observer.disconnect();