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 const controllerURL = Cypress.env("controller_url");
32 const systemToken = Cypress.env("system_token");
33 let createdResources = [];
35 // Clean up on a 'before' hook to allow post-mortem analysis on individual tests.
36 beforeEach(function () {
37 if (createdResources.length === 0) {
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);
46 createdResources = [];
51 (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
54 url: `${controllerURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`,
56 qs: auth ? qs : Object.assign({ api_token: token }, qs),
57 auth: auth ? { bearer: `${token}` } : undefined,
58 followRedirect: followRedirect,
59 failOnStatusCode: failOnStatusCode,
64 Cypress.Commands.add("getUser", (username, first_name = "", last_name = "", is_admin = false, is_active = true) => {
65 // Create user if not already created
70 "/auth/controller/callback",
72 auth_info: JSON.stringify({
73 email: `${username}@example.local`,
75 first_name: first_name,
79 return_to: ",https://example.local",
85 ) // Don't follow redirects so we can catch the token
86 .its("headers.location")
88 // Get its token and set the account up as admin and/or active
90 this.userToken = this.location.split("=")[1];
91 assert.isString(this.userToken);
93 .doRequest("GET", "/arvados/v1/users", null, {
94 filters: `[["username", "=", "${username}"]]`,
99 cy.doRequest("PUT", `/arvados/v1/users/${this.aUser.uuid}`, {
102 is_active: is_active,
108 cy.doRequest("GET", "/arvados/v1/api_clients", null, {
109 filters: `[["is_trusted", "=", false]]`,
110 order: `["created_at desc"]`,
115 if (this.apiClients.length > 0) {
116 cy.doRequest("PUT", `/arvados/v1/api_clients/${this.apiClients[0].uuid}`, {
122 .as("updatedApiClient")
124 assert(this.updatedApiClient.is_trusted);
129 return { user: this.theUser, token: this.userToken };
137 Cypress.Commands.add("createLink", (token, data) => {
138 return cy.createResource(token, "links", {
139 link: JSON.stringify(data),
143 Cypress.Commands.add("createGroup", (token, data) => {
144 return cy.createResource(token, "groups", {
145 group: JSON.stringify(data),
146 ensure_unique_name: true,
150 Cypress.Commands.add("trashGroup", (token, uuid) => {
151 return cy.deleteResource(token, "groups", uuid);
154 Cypress.Commands.add("createWorkflow", (token, data) => {
155 return cy.createResource(token, "workflows", {
156 workflow: JSON.stringify(data),
157 ensure_unique_name: true,
161 Cypress.Commands.add("getCollection", (token, uuid) => {
162 return cy.getResource(token, "collections", uuid);
165 Cypress.Commands.add("createCollection", (token, data) => {
166 return cy.createResource(token, "collections", {
167 collection: JSON.stringify(data),
168 ensure_unique_name: true,
172 Cypress.Commands.add("updateCollection", (token, uuid, data) => {
173 return cy.updateResource(token, "collections", uuid, {
174 collection: JSON.stringify(data),
178 Cypress.Commands.add("getContainer", (token, uuid) => {
179 return cy.getResource(token, "containers", uuid);
182 Cypress.Commands.add("updateContainer", (token, uuid, data) => {
183 return cy.updateResource(token, "containers", uuid, {
184 container: JSON.stringify(data),
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,
195 Cypress.Commands.add("updateContainerRequest", (token, uuid, data) => {
196 return cy.updateResource(token, "container_requests", uuid, {
197 container_request: JSON.stringify(data),
201 Cypress.Commands.add("createLog", (token, data) => {
202 return cy.createResource(token, "logs", {
203 log: JSON.stringify(data),
207 Cypress.Commands.add("logsForContainer", (token, uuid, logType, logTextArray = []) => {
209 for (const logText of logTextArray) {
222 cy.getAll("@lastLogRecord").then(function () {
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,
234 Cypress.Commands.add("getResource", (token, suffix, uuid) => {
236 .doRequest("GET", `/arvados/v1/${suffix}/${uuid}`, null, {}, token)
238 .then(function (resource) {
243 Cypress.Commands.add("createResource", (token, suffix, data) => {
245 .doRequest("POST", "/arvados/v1/" + suffix, data, null, token, true)
247 .then(function (resource) {
248 createdResources.push({ suffix, uuid: resource.uuid });
253 Cypress.Commands.add("deleteResource", (token, suffix, uuid, failOnStatusCode = true) => {
255 .doRequest("DELETE", "/arvados/v1/" + suffix + "/" + uuid, null, null, token, false, true, failOnStatusCode)
257 .then(function (resource) {
262 Cypress.Commands.add("updateResource", (token, suffix, uuid, data) => {
264 .doRequest("PATCH", "/arvados/v1/" + suffix + "/" + uuid, data, null, token, true)
266 .then(function (resource) {
271 Cypress.Commands.add("loginAs", user => {
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");
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")
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]")
289 .type(newDescription);
290 cy.get("[data-cy=form-submit-btn]").click();
293 cy.get(container).contains(newName).rightclick();
294 cy.get("[data-cy=context-menu]")
295 .contains(isProject ? "Edit project" : "Edit collection")
297 cy.get("[data-cy=form-dialog]").within(() => {
298 cy.get("input[name=name]").should("have.value", newName);
301 cy.get("span[data-text=true]").contains(newDescription);
303 cy.get("input[name=description]").should("have.value", newDescription);
306 cy.get("[data-cy=form-cancel-btn]").click();
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();
315 Cypress.Commands.add("goToPath", path => {
316 return cy.window().its("appHistory").invoke("push", path);
319 Cypress.Commands.add("getAll", (...elements) => {
320 const promise = cy.wrap([], { log: false });
322 for (let element of elements) {
323 promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got])));
329 Cypress.Commands.add("shareWith", (srcUserToken, targetUserUUID, itemUUID, permission = "can_write") => {
330 cy.createLink(srcUserToken, {
332 link_class: "permission",
334 tail_uuid: targetUserUUID,
338 Cypress.Commands.add("addToFavorites", (userToken, userUUID, itemUUID) => {
339 cy.createLink(userToken, {
343 owner_uuid: userUUID,
348 Cypress.Commands.add("createProject", ({ owningUser, targetUser, projectName, canWrite, addToFavorites }) => {
349 const writePermission = canWrite ? "can_write" : "can_read";
351 cy.createGroup(owningUser.token, {
352 name: `${projectName} ${Math.floor(Math.random() * 999999)}`,
353 group_class: "project",
355 .as(`${projectName}`)
357 if (targetUser && targetUser !== owningUser) {
358 cy.shareWith(owningUser.token, targetUser.user.uuid, project.uuid, writePermission);
360 if (addToFavorites) {
361 const user = targetUser ? targetUser : owningUser;
362 cy.addToFavorites(user.token, user.user.uuid, project.uuid);
367 Cypress.Commands.add(
370 prevSubject: "element",
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);
377 cy.wrap(subject).trigger("drop", {
378 dataTransfer: { files: [testFile] },
384 function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
385 const byteCharacters = atob(b64Data);
386 const byteArrays = [];
388 for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
389 const slice = byteCharacters.slice(offset, offset + sliceSize);
391 const byteNumbers = new Array(slice.length);
392 for (let i = 0; i < slice.length; i++) {
393 byteNumbers[i] = slice.charCodeAt(i);
396 const byteArray = new Uint8Array(byteNumbers);
398 byteArrays.push(byteArray);
401 const blob = new Blob(byteArrays, { type: contentType });
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", () => {
410 // Don't timeout before waitForDom finishes
416 cy.log("Waiting for DOM mutations to complete");
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;
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;
438 // initialize the previousMutationCount
439 if (win.previousMutationCount == null) win.previousMutationCount = 0;
442 // watch the document body for the specified mutations
443 observer.observe(win.document.body, observerConfig);
445 // check the DOM for mutations up to 50 times for a maximum time of 5 seconds
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;
452 // make each iteration of the loop 100ms apart
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;
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.");
466 // proceed to the next iteration
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`);
475 // disconnect the observer and resolve the promise
476 observer.disconnect();