X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/ca7a29e2ac03703afeff3248d1b909f42b89ab19..e10e24d713e5c31cbc34efca458f5718eb6eb000:/cypress/support/commands.js diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 8c6fd462..46d77fe8 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -28,89 +28,532 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) -const controllerURL = Cypress.env('controller_url'); -const systemToken = Cypress.env('system_token'); +import { extractFilesData } from "services/collection-service/collection-service-files-response"; + +const controllerURL = Cypress.env("controller_url"); +const systemToken = Cypress.env("system_token"); +let createdResources = []; + +const containerLogFolderPrefix = "log for container "; + +// Clean up anything that was created. You can temporarily add +// 'return' to the top if you need the resources to hang around to +// debug a specific test. +afterEach(function () { + if (createdResources.length === 0) { + return; + } + cy.log(`Cleaning ${createdResources.length} previously created resource(s).`); + createdResources.forEach(function ({ suffix, uuid }) { + // Don't fail when a resource isn't already there, some objects may have + // been removed, directly or indirectly, from the test that created them. + cy.deleteResource(systemToken, suffix, uuid, false); + }); + createdResources = []; +}); Cypress.Commands.add( - "do_request", (method='GET', path='', data=null, qs=null, - token=systemToken, auth=false, followRedirect=true) => { + "doRequest", + (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => { return cy.request({ method: method, - url: `${controllerURL}/${path}`, + url: `${controllerURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`, body: data, - qs: auth ? qs : Object.assign({api_token: token}, qs), - auth: auth ? {bearer: `${token}`} : undefined, - followRedirect: followRedirect - }) + qs: auth ? qs : Object.assign({ api_token: token }, qs), + auth: auth ? { bearer: `${token}` } : undefined, + followRedirect: followRedirect, + failOnStatusCode: failOnStatusCode, + }); } -) +); + +Cypress.Commands.add("getUser", (username, first_name = "", last_name = "", is_admin = false, is_active = true) => { + // Create user if not already created + return ( + cy + .doRequest( + "POST", + "/auth/controller/callback", + { + auth_info: JSON.stringify({ + email: `${username}@example.local`, + username: username, + first_name: first_name, + last_name: last_name, + alternate_emails: [], + }), + return_to: ",https://example.local", + }, + null, + systemToken, + true, + false + ) // Don't follow redirects so we can catch the token + .its("headers.location") + .as("location") + // Get its token and set the account up as admin and/or active + .then(function () { + this.userToken = this.location.split("=")[1]; + assert.isString(this.userToken); + return cy + .doRequest("GET", "/arvados/v1/users", null, { + filters: `[["username", "=", "${username}"]]`, + }) + .its("body.items.0") + .as("aUser") + .then(function () { + cy.doRequest("PUT", `/arvados/v1/users/${this.aUser.uuid}`, { + user: { + is_admin: is_admin, + is_active: is_active, + }, + }) + .its("body") + .as("theUser") + .then(function () { + cy.doRequest("GET", "/arvados/v1/api_clients", null, { + filters: `[["is_trusted", "=", false]]`, + order: `["created_at desc"]`, + }) + .its("body.items") + .as("apiClients") + .then(function () { + if (this.apiClients.length > 0) { + cy.doRequest("PUT", `/arvados/v1/api_clients/${this.apiClients[0].uuid}`, { + api_client: { + is_trusted: true, + }, + }) + .its("body") + .as("updatedApiClient") + .then(function () { + assert(this.updatedApiClient.is_trusted); + }); + } + }) + .then(function () { + return { user: this.theUser, token: this.userToken }; + }); + }); + }); + }) + ); +}); + +Cypress.Commands.add("createLink", (token, data) => { + return cy.createResource(token, "links", { + link: JSON.stringify(data), + }); +}); -// This resets the DB removing all content and seeding it with the fixtures. -// TODO: Maybe we can add an optional param to avoid the loading part? Cypress.Commands.add( - "resetDB", () => { - cy.request('POST', `${controllerURL}/database/reset?api_token=${systemToken}`); + "doWebDAVRequest", + (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => { + return cy.doRequest("GET", "/arvados/v1/config", null, null).then(({ body: config }) => { + return cy.request({ + method: method, + url: `${config.Services.WebDAVDownload.ExternalURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`, + body: data, + qs: auth ? qs : Object.assign({ api_token: token }, qs), + auth: auth ? { bearer: `${token}` } : undefined, + followRedirect: followRedirect, + failOnStatusCode: failOnStatusCode, + }); + }); } -) +); -Cypress.Commands.add( - "getUser", (username, first_name='', last_name='', is_admin=false, is_active=true) => { - // Create user if not already created - return cy.do_request('POST', '/auth/controller/callback', { - auth_info: JSON.stringify({ - email: `${username}@example.local`, - username: username, - first_name: first_name, - last_name: last_name, - alternate_emails: [] - }), - return_to: ',https://example.local' - }, null, systemToken, true, false) // Don't follow redirects so we can catch the token - .its('headers.location').as('location') - // Get its token and set the account up as admin and/or active - .then(function() { - this.userToken = this.location.split("=")[1] - assert.isString(this.userToken) - return cy.do_request('GET', '/arvados/v1/users', null, { - filters: `[["username", "=", "${username}"]]` - }) - .its('body.items.0') - .as('aUser') - .then(function() { - cy.do_request('PUT', `/arvados/v1/users/${this.aUser.uuid}`, { - user: { - is_admin: is_admin, - is_active: is_active - } +Cypress.Commands.add("getUser", (username, first_name = "", last_name = "", is_admin = false, is_active = true) => { + // Create user if not already created + return ( + cy + .doRequest( + "POST", + "/auth/controller/callback", + { + auth_info: JSON.stringify({ + email: `${username}@example.local`, + username: username, + first_name: first_name, + last_name: last_name, + alternate_emails: [], + }), + return_to: ",https://example.local", + }, + null, + systemToken, + true, + false + ) // Don't follow redirects so we can catch the token + .its("headers.location") + .as("location") + // Get its token and set the account up as admin and/or active + .then( + function () { + this.userToken = this.location.split("=")[1]; + assert.isString(this.userToken); + return cy + .doRequest("GET", "/arvados/v1/users", null, { + filters: `[["username", "=", "${username}"]]`, + }) + .its("body.items.0") + .as("aUser") + .then(function () { + cy.doRequest("PUT", `/arvados/v1/users/${this.aUser.uuid}`, { + user: { + is_admin: is_admin, + is_active: is_active, + }, + }).as("lastLogRecord"); + }); + }, + cy.getAll("@lastLogRecord").then(function () { + return logs; }) - .its('body') - .as('theUser') - .then(function() { - return {user: this.theUser, token: this.userToken}; + ) + ); +}); + +Cypress.Commands.add("createVirtualMachine", (token, data) => { + return cy.createResource(token, "virtual_machines", { + virtual_machine: JSON.stringify(data), + ensure_unique_name: true, + }); +}); + +Cypress.Commands.add("getResource", (token, suffix, uuid) => { + return cy + .doRequest("GET", `/arvados/v1/${suffix}/${uuid}`, null, {}, token) + .its("body") + .then(function (resource) { + return resource; + }); +}); + +Cypress.Commands.add("createResource", (token, suffix, data) => { + return cy + .doRequest("POST", "/arvados/v1/" + suffix, data, null, token, true) + .its("body") + .then(function (resource) { + createdResources.push({ suffix, uuid: resource.uuid }); + return resource; + }); +}); + +Cypress.Commands.add("deleteResource", (token, suffix, uuid, failOnStatusCode = true) => { + return cy + .doRequest("DELETE", "/arvados/v1/" + suffix + "/" + uuid, null, null, token, false, true, failOnStatusCode) + .its("body") + .then(function (resource) { + return resource; + }); +}); + +Cypress.Commands.add("updateResource", (token, suffix, uuid, data) => { + return cy + .doRequest("PATCH", "/arvados/v1/" + suffix + "/" + uuid, data, null, token, true) + .its("body") + .then(function (resource) { + return resource; + }); +}); + +Cypress.Commands.add("createCollection", (token, data) => { + return cy.createResource(token, "collections", { + collection: JSON.stringify(data), + ensure_unique_name: true, + }); +}); + +Cypress.Commands.add("getCollection", (token, uuid) => { + return cy.getResource(token, "collections", uuid); +}); + +Cypress.Commands.add("updateCollection", (token, uuid, data) => { + return cy.updateResource(token, "collections", uuid, { + collection: JSON.stringify(data), + }); +}); + +Cypress.Commands.add("collectionReplaceFiles", (token, uuid, data) => { + return cy.updateResource(token, "collections", uuid, { + collection: { + preserve_version: true, + }, + replace_files: JSON.stringify(data), + }); +}); + +Cypress.Commands.add("getContainer", (token, uuid) => { + return cy.getResource(token, "containers", uuid); +}); + +Cypress.Commands.add("updateContainer", (token, uuid, data) => { + return cy.updateResource(token, "containers", uuid, { + container: JSON.stringify(data), + }); +}); + +Cypress.Commands.add("getContainerRequest", (token, uuid) => { + return cy.getResource(token, "container_requests", uuid); +}); + +Cypress.Commands.add("createContainerRequest", (token, data) => { + return cy.createResource(token, "container_requests", { + container_request: JSON.stringify(data), + ensure_unique_name: true, + }); +}); + +Cypress.Commands.add("updateContainerRequest", (token, uuid, data) => { + return cy.updateResource(token, "container_requests", uuid, { + container_request: JSON.stringify(data), + }); +}); + +/** + * Requires an admin token for log_uuid modification to succeed + */ +Cypress.Commands.add("appendLog", (token, crUuid, fileName, lines = []) => + cy.getContainerRequest(token, crUuid).then(containerRequest => { + if (containerRequest.log_uuid) { + cy.listContainerRequestLogs(token, crUuid).then(logFiles => { + const filePath = `${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`; + if (logFiles.find(file => file.name === fileName)) { + // File exists, fetch and append + return cy + .doWebDAVRequest("GET", `c=${filePath}`, null, null, token) + .then(({ body: contents }) => + cy.doWebDAVRequest("PUT", `c=${filePath}`, contents.split("\n").concat(lines).join("\n"), null, token) + ); + } else { + // File not exists, put new file + cy.doWebDAVRequest("PUT", `c=${filePath}`, lines.join("\n"), null, token); + } + }); + } else { + // Create log collection + return cy + .createCollection(token, { + name: `Test log collection ${Math.floor(Math.random() * 999999)}`, + owner_uuid: containerRequest.owner_uuid, + manifest_text: "", }) + .then(collection => { + // Update CR log_uuid to fake log collection + cy.updateContainerRequest(token, containerRequest.uuid, { + log_uuid: collection.uuid, + }).then(() => + // Create empty directory for container uuid + cy + .collectionReplaceFiles(token, collection.uuid, { + [`/${containerLogFolderPrefix}${containerRequest.container_uuid}`]: "d41d8cd98f00b204e9800998ecf8427e+0", + }) + .then(() => + // Put new log file with contents into fake log collection + cy.doWebDAVRequest( + "PUT", + `c=${collection.uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`, + lines.join("\n"), + null, + token + ) + ) + ); + }); + } + }) +); + +Cypress.Commands.add("listContainerRequestLogs", (token, crUuid) => + cy.getContainerRequest(token, crUuid).then(containerRequest => + cy + .doWebDAVRequest( + "PROPFIND", + `c=${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}`, + null, + null, + token + ) + .then(({ body: data }) => { + return extractFilesData(new DOMParser().parseFromString(data, "text/xml")); }) - }) + ) +); + +cy.get("[data-cy=form-cancel-btn]").click(); + +Cypress.Commands.add("doSearch", searchTerm => { + cy.get("[data-cy=searchbar-input-field]").type(`{selectall}${searchTerm}{enter}`); + cy.get("[data-cy=searchbar-parent-form]").submit(); +}); + +Cypress.Commands.add("goToPath", path => { + return cy.window().its("appHistory").invoke("push", path); +}); + +Cypress.Commands.add("getAll", (...elements) => { + const promise = cy.wrap([], { log: false }); + + for (let element of elements) { + promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got]))); } -) + + return promise; +}); + +Cypress.Commands.add("shareWith", (srcUserToken, targetUserUUID, itemUUID, permission = "can_write") => { + cy.createLink(srcUserToken, { + name: permission, + link_class: "permission", + head_uuid: itemUUID, + tail_uuid: targetUserUUID, + }); +}); + +Cypress.Commands.add("addToFavorites", (userToken, userUUID, itemUUID) => { + cy.createLink(userToken, { + head_uuid: itemUUID, + link_class: "star", + name: "", + owner_uuid: userUUID, + tail_uuid: userUUID, + }); +}); + +Cypress.Commands.add("createProject", ({ owningUser, targetUser, projectName, canWrite, addToFavorites }) => { + const writePermission = canWrite ? "can_write" : "can_read"; + + cy.createGroup(owningUser.token, { + name: `${projectName} ${Math.floor(Math.random() * 999999)}`, + group_class: "project", + }) + .as(`${projectName}`) + .then(project => { + if (targetUser && targetUser !== owningUser) { + cy.shareWith(owningUser.token, targetUser.user.uuid, project.uuid, writePermission); + } + if (addToFavorites) { + const user = targetUser ? targetUser : owningUser; + cy.addToFavorites(user.token, user.user.uuid, project.uuid); + } + }); +}); Cypress.Commands.add( - "createCollection", (token, collection) => { - return cy.do_request('POST', '/arvados/v1/collections', { - collection: JSON.stringify(collection), - ensure_unique_name: true - }, null, token, true) - .its('body').as('collection') - .then(function() { - return this.collection; - }) + "upload", + { + prevSubject: "element", + }, + (subject, file, fileName, binaryMode = true) => { + cy.window().then(window => { + const blob = binaryMode ? b64toBlob(file, "", 512) : new Blob([file], { type: "text/plain" }); + const testFile = new window.File([blob], fileName); + + cy.wrap(subject).trigger("drop", { + dataTransfer: { files: [testFile] }, + }); + }); } -) +); -Cypress.Commands.add( - "loginAs", (user) => { - cy.visit(`/token/?api_token=${user.token}`); - cy.url().should('contain', '/projects/'); - cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)'); - cy.get('div#root').should('not.contain', 'Your account is inactive'); +function b64toBlob(b64Data, contentType = "", sliceSize = 512) { + const byteCharacters = atob(b64Data); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + + byteArrays.push(byteArray); } -) \ No newline at end of file + + const blob = new Blob(byteArrays, { type: contentType }); + return blob; +} + +// From https://github.com/cypress-io/cypress/issues/7306#issuecomment-1076451070= +// This command requires the async package (https://www.npmjs.com/package/async) +Cypress.Commands.add("waitForDom", () => { + cy.window().then( + { + // Don't timeout before waitForDom finishes + timeout: 10000, + }, + win => { + let timeElapsed = 0; + + cy.log("Waiting for DOM mutations to complete"); + + return new Cypress.Promise(resolve => { + // set the required variables + let async = require("async"); + let observerConfig = { attributes: true, childList: true, subtree: true }; + let items = Array.apply(null, { length: 50 }).map(Number.call, Number); + win.mutationCount = 0; + win.previousMutationCount = null; + + // create an observer instance + let observer = new win.MutationObserver(mutations => { + mutations.forEach(mutation => { + // Only record "attributes" type mutations that are not a "class" mutation. + // If the mutation is not an "attributes" type, then we always record it. + if (mutation.type === "attributes" && mutation.attributeName !== "class") { + win.mutationCount += 1; + } else if (mutation.type !== "attributes") { + win.mutationCount += 1; + } + }); + + // initialize the previousMutationCount + if (win.previousMutationCount == null) win.previousMutationCount = 0; + }); + + // watch the document body for the specified mutations + observer.observe(win.document.body, observerConfig); + + // check the DOM for mutations up to 50 times for a maximum time of 5 seconds + async.eachSeries( + items, + function iteratee(item, callback) { + // keep track of the elapsed time so we can log it at the end of the command + timeElapsed = timeElapsed + 100; + + // make each iteration of the loop 100ms apart + setTimeout(() => { + if (win.mutationCount === win.previousMutationCount) { + // pass an argument to the async callback to exit the loop + return callback("Resolved - DOM changes complete."); + } else if (win.previousMutationCount != null) { + // only set the previous count if the observer has checked the DOM at least once + win.previousMutationCount = win.mutationCount; + return callback(); + } else if (win.mutationCount === 0 && win.previousMutationCount == null && item === 4) { + // this is an early exit in case nothing is changing in the DOM. That way we only + // wait 500ms instead of the full 5 seconds when no DOM changes are occurring. + return callback("Resolved - Exiting early since no DOM changes were detected."); + } else { + // proceed to the next iteration + return callback(); + } + }, 100); + }, + function done() { + // Log the total wait time so users can see it + cy.log(`DOM mutations ${timeElapsed >= 5000 ? "did not complete" : "completed"} in ${timeElapsed} ms`); + + // disconnect the observer and resolve the promise + observer.disconnect(); + resolve(); + } + ); + }); + } + ); +});