From 26d627007be2bd8fc68844b43651b1897a56e391 Mon Sep 17 00:00:00 2001 From: Lisa Knox Date: Wed, 20 Sep 2023 10:56:16 -0400 Subject: [PATCH] 15768: added manual form submit in searchbar spec Arvados-DCO-1.1-Signed-off-by: Lisa Knox --- cypress/support/commands.js | 725 +++++++++--------- .../search-bar/search-bar-view.tsx | 178 ++--- 2 files changed, 446 insertions(+), 457 deletions(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index f09d959b..199b5c13 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -28,8 +28,8 @@ // -- 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'); +const controllerURL = Cypress.env("controller_url"); +const systemToken = Cypress.env("system_token"); let createdResources = []; // Clean up on a 'before' hook to allow post-mortem analysis on individual tests. @@ -38,7 +38,7 @@ beforeEach(function () { return; } cy.log(`Cleaning ${createdResources.length} previously created resource(s)`); - createdResources.forEach(function({suffix, uuid}) { + 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); @@ -47,363 +47,343 @@ beforeEach(function () { }); Cypress.Commands.add( - "doRequest", (method = 'GET', path = '', data = null, qs = null, - token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => { - return cy.request({ - method: method, - url: `${controllerURL.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.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') + "doRequest", + (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => { + return cy.request({ + method: method, + url: `${controllerURL.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 + .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 () { - 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"]` + this.userToken = this.location.split("=")[1]; + assert.isString(this.userToken); + return cy + .doRequest("GET", "/arvados/v1/users", null, { + filters: `[["username", "=", "${username}"]]`, }) - .its('body.items').as('apiClients') + .its("body.items.0") + .as("aUser") .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 }; - }) - }) + 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) - }) - } -) - -Cypress.Commands.add( - "createGroup", (token, data) => { - return cy.createResource(token, 'groups', { - group: JSON.stringify(data), - ensure_unique_name: true - }) - } -) - -Cypress.Commands.add( - "trashGroup", (token, uuid) => { - return cy.deleteResource(token, 'groups', uuid); - } -) + ); +}); +Cypress.Commands.add("createLink", (token, data) => { + return cy.createResource(token, "links", { + link: JSON.stringify(data), + }); +}); -Cypress.Commands.add( - "createWorkflow", (token, data) => { - return cy.createResource(token, 'workflows', { - workflow: JSON.stringify(data), - ensure_unique_name: true - }) - } -) +Cypress.Commands.add("createGroup", (token, data) => { + return cy.createResource(token, "groups", { + group: JSON.stringify(data), + ensure_unique_name: true, + }); +}); -Cypress.Commands.add( - "getCollection", (token, uuid) => { - return cy.getResource(token, 'collections', uuid) - } -) +Cypress.Commands.add("trashGroup", (token, uuid) => { + return cy.deleteResource(token, "groups", uuid); +}); -Cypress.Commands.add( - "createCollection", (token, data) => { - return cy.createResource(token, 'collections', { - collection: JSON.stringify(data), - ensure_unique_name: true - }) - } -) +Cypress.Commands.add("createWorkflow", (token, data) => { + return cy.createResource(token, "workflows", { + workflow: JSON.stringify(data), + ensure_unique_name: true, + }); +}); -Cypress.Commands.add( - "updateCollection", (token, uuid, data) => { - return cy.updateResource(token, 'collections', uuid, { - collection: JSON.stringify(data) - }) - } -) +Cypress.Commands.add("getCollection", (token, uuid) => { + return cy.getResource(token, "collections", uuid); +}); -Cypress.Commands.add( - "getContainer", (token, uuid) => { - return cy.getResource(token, 'containers', uuid) - } -) +Cypress.Commands.add("createCollection", (token, data) => { + return cy.createResource(token, "collections", { + collection: JSON.stringify(data), + ensure_unique_name: true, + }); +}); -Cypress.Commands.add( - "updateContainer", (token, uuid, data) => { - return cy.updateResource(token, 'containers', uuid, { - container: JSON.stringify(data) - }) - } -) +Cypress.Commands.add("updateCollection", (token, uuid, data) => { + return cy.updateResource(token, "collections", uuid, { + collection: JSON.stringify(data), + }); +}); -Cypress.Commands.add( - 'createContainerRequest', (token, data) => { - return cy.createResource(token, 'container_requests', { - container_request: JSON.stringify(data), - ensure_unique_name: true - }) - } -) +Cypress.Commands.add("getContainer", (token, uuid) => { + return cy.getResource(token, "containers", uuid); +}); -Cypress.Commands.add( - "updateContainerRequest", (token, uuid, data) => { - return cy.updateResource(token, 'container_requests', uuid, { - container_request: JSON.stringify(data) - }) - } -) +Cypress.Commands.add("updateContainer", (token, uuid, data) => { + return cy.updateResource(token, "containers", uuid, { + container: JSON.stringify(data), + }); +}); -Cypress.Commands.add( - "createLog", (token, data) => { - return cy.createResource(token, 'logs', { - log: JSON.stringify(data) - }) - } -) +Cypress.Commands.add("createContainerRequest", (token, data) => { + return cy.createResource(token, "container_requests", { + container_request: JSON.stringify(data), + ensure_unique_name: true, + }); +}); -Cypress.Commands.add( - "logsForContainer", (token, uuid, logType, logTextArray = []) => { - let logs = []; - for (const logText of logTextArray) { - logs.push(cy.createLog(token, { - object_uuid: uuid, - event_type: logType, - properties: { - text: logText - } - }).as('lastLogRecord')) - } - cy.getAll('@lastLogRecord').then(function () { - return logs; - }) - } -) +Cypress.Commands.add("updateContainerRequest", (token, uuid, data) => { + return cy.updateResource(token, "container_requests", uuid, { + container_request: JSON.stringify(data), + }); +}); -Cypress.Commands.add( - "createVirtualMachine", (token, data) => { - return cy.createResource(token, 'virtual_machines', { - virtual_machine: JSON.stringify(data), - ensure_unique_name: true - }) - } -) +Cypress.Commands.add("createLog", (token, data) => { + return cy.createResource(token, "logs", { + log: JSON.stringify(data), + }); +}); -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("logsForContainer", (token, uuid, logType, logTextArray = []) => { + let logs = []; + for (const logText of logTextArray) { + logs.push( + cy + .createLog(token, { + object_uuid: uuid, + event_type: logType, + properties: { + text: logText, + }, + }) + .as("lastLogRecord") + ); } -) + cy.getAll("@lastLogRecord").then(function () { + return logs; + }); +}); -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("createVirtualMachine", (token, data) => { + return cy.createResource(token, "virtual_machines", { + virtual_machine: JSON.stringify(data), + ensure_unique_name: true, + }); +}); -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("getResource", (token, suffix, uuid) => { + return cy + .doRequest("GET", `/arvados/v1/${suffix}/${uuid}`, null, {}, token) + .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("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( - "loginAs", (user) => { - cy.clearCookies() - cy.clearLocalStorage() - cy.visit(`/token/?api_token=${user.token}`); - cy.url({timeout: 10000}).should('contain', '/projects/'); - cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)'); - cy.get('div#root').should('not.contain', 'Your account is inactive'); - } -) +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( - "testEditProjectOrCollection", (container, oldName, newName, newDescription, isProject = true) => { - cy.get(container).contains(oldName).rightclick(); - cy.get('[data-cy=context-menu]').contains(isProject ? 'Edit project' : 'Edit collection').click(); - cy.get('[data-cy=form-dialog]').within(() => { - cy.get('input[name=name]').clear().type(newName); - cy.get(isProject ? 'div[contenteditable=true]' : 'input[name=description]').clear().type(newDescription); - cy.get('[data-cy=form-submit-btn]').click(); +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; }); +}); - cy.get(container).contains(newName).rightclick(); - cy.get('[data-cy=context-menu]').contains(isProject ? 'Edit project' : 'Edit collection').click(); - cy.get('[data-cy=form-dialog]').within(() => { - cy.get('input[name=name]').should('have.value', newName); +Cypress.Commands.add("loginAs", user => { + cy.clearCookies(); + cy.clearLocalStorage(); + cy.visit(`/token/?api_token=${user.token}`); + cy.url({ timeout: 10000 }).should("contain", "/projects/"); + cy.get("div#root").should("contain", "Arvados Workbench (zzzzz)"); + cy.get("div#root").should("not.contain", "Your account is inactive"); +}); - if (isProject) { - cy.get('span[data-text=true]').contains(newDescription); - } else { - cy.get('input[name=description]').should('have.value', newDescription); - } +Cypress.Commands.add("testEditProjectOrCollection", (container, oldName, newName, newDescription, isProject = true) => { + cy.get(container).contains(oldName).rightclick(); + cy.get("[data-cy=context-menu]") + .contains(isProject ? "Edit project" : "Edit collection") + .click(); + cy.get("[data-cy=form-dialog]").within(() => { + cy.get("input[name=name]").clear().type(newName); + cy.get(isProject ? "div[contenteditable=true]" : "input[name=description]") + .clear() + .type(newDescription); + cy.get("[data-cy=form-submit-btn]").click(); + }); - cy.get('[data-cy=form-cancel-btn]').click(); - }); - } -) + cy.get(container).contains(newName).rightclick(); + cy.get("[data-cy=context-menu]") + .contains(isProject ? "Edit project" : "Edit collection") + .click(); + cy.get("[data-cy=form-dialog]").within(() => { + cy.get("input[name=name]").should("have.value", newName); + + if (isProject) { + cy.get("span[data-text=true]").contains(newDescription); + } else { + cy.get("input[name=description]").should("have.value", newDescription); + } -Cypress.Commands.add( - "doSearch", (searchTerm) => { - cy.get('[data-cy=searchbar-input-field]').type(`{selectall}${searchTerm}{enter}`); - } -) + cy.get("[data-cy=form-cancel-btn]").click(); + }); +}); -Cypress.Commands.add( - "goToPath", (path) => { - return cy.window().its('appHistory').invoke('push', path); - } -) +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('getAll', (...elements) => { - const promise = cy.wrap([], { log: false }) +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]))) + promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got]))); } - return promise -}) + return promise; +}); -Cypress.Commands.add('shareWith', (srcUserToken, targetUserUUID, itemUUID, permission = 'can_write') => { +Cypress.Commands.add("shareWith", (srcUserToken, targetUserUUID, itemUUID, permission = "can_write") => { cy.createLink(srcUserToken, { name: permission, - link_class: 'permission', + link_class: "permission", head_uuid: itemUUID, - tail_uuid: targetUserUUID + tail_uuid: targetUserUUID, }); -}) +}); -Cypress.Commands.add('addToFavorites', (userToken, userUUID, itemUUID) => { +Cypress.Commands.add("addToFavorites", (userToken, userUUID, itemUUID) => { cy.createLink(userToken, { head_uuid: itemUUID, - link_class: 'star', - name: '', + 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'; +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); - } - }); + 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( - 'upload', + "upload", { - prevSubject: 'element', + prevSubject: "element", }, (subject, file, fileName, binaryMode = true) => { cy.window().then(window => { - const blob = binaryMode - ? b64toBlob(file, '', 512) - : new Blob([file], {type: 'text/plain'}); + const blob = binaryMode ? b64toBlob(file, "", 512) : new Blob([file], { type: "text/plain" }); const testFile = new window.File([blob], fileName); - cy.wrap(subject).trigger('drop', { + cy.wrap(subject).trigger("drop", { dataTransfer: { files: [testFile] }, }); - }) + }); } -) +); -function b64toBlob(b64Data, contentType = '', sliceSize = 512) { - const byteCharacters = atob(b64Data) - const byteArrays = [] +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); @@ -419,78 +399,85 @@ function b64toBlob(b64Data, contentType = '', sliceSize = 512) { } const blob = new Blob(byteArrays, { type: contentType }); - return blob + 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(); - }); - }); - }); - }); +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(); + } + ); + }); + } + ); +}); diff --git a/src/views-components/search-bar/search-bar-view.tsx b/src/views-components/search-bar/search-bar-view.tsx index 28408347..eba281c9 100644 --- a/src/views-components/search-bar/search-bar-view.tsx +++ b/src/views-components/search-bar/search-bar-view.tsx @@ -2,73 +2,61 @@ // // SPDX-License-Identifier: AGPL-3.0 -import React from 'react'; -import { compose } from 'redux'; -import { - IconButton, - Paper, - StyleRulesCallback, - withStyles, - WithStyles, - Tooltip, - InputAdornment, Input, -} from '@material-ui/core'; -import SearchIcon from '@material-ui/icons/Search'; -import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; -import { ArvadosTheme } from 'common/custom-theme'; -import { SearchView } from 'store/search-bar/search-bar-reducer'; -import { - SearchBarBasicView, - SearchBarBasicViewDataProps, - SearchBarBasicViewActionProps -} from 'views-components/search-bar/search-bar-basic-view'; +import React from "react"; +import { compose } from "redux"; +import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, Tooltip, InputAdornment, Input } from "@material-ui/core"; +import SearchIcon from "@material-ui/icons/Search"; +import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; +import { ArvadosTheme } from "common/custom-theme"; +import { SearchView } from "store/search-bar/search-bar-reducer"; +import { SearchBarBasicView, SearchBarBasicViewDataProps, SearchBarBasicViewActionProps } from "views-components/search-bar/search-bar-basic-view"; import { SearchBarAutocompleteView, SearchBarAutocompleteViewDataProps, - SearchBarAutocompleteViewActionProps -} from 'views-components/search-bar/search-bar-autocomplete-view'; + SearchBarAutocompleteViewActionProps, +} from "views-components/search-bar/search-bar-autocomplete-view"; import { SearchBarAdvancedView, SearchBarAdvancedViewDataProps, - SearchBarAdvancedViewActionProps -} from 'views-components/search-bar/search-bar-advanced-view'; + SearchBarAdvancedViewActionProps, +} from "views-components/search-bar/search-bar-advanced-view"; import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "common/codes"; -import { debounce } from 'debounce'; -import { Vocabulary } from 'models/vocabulary'; -import { connectVocabulary } from '../resource-properties-form/property-field-common'; +import { debounce } from "debounce"; +import { Vocabulary } from "models/vocabulary"; +import { connectVocabulary } from "../resource-properties-form/property-field-common"; -type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view'; +type CssRules = "container" | "containerSearchViewOpened" | "input" | "view"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => { return { container: { - position: 'relative', - width: '100%', + position: "relative", + width: "100%", borderRadius: theme.spacing.unit / 2, zIndex: theme.zIndex.modal, }, containerSearchViewOpened: { - position: 'relative', - width: '100%', + position: "relative", + width: "100%", borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`, zIndex: theme.zIndex.modal, }, input: { - border: 'none', - padding: `0` + border: "none", + padding: `0`, }, view: { - position: 'absolute', - width: '100%', - zIndex: 1 - } + position: "absolute", + width: "100%", + zIndex: 1, + }, }; }; -export type SearchBarDataProps = SearchBarViewDataProps - & SearchBarAutocompleteViewDataProps - & SearchBarAdvancedViewDataProps - & SearchBarBasicViewDataProps; +export type SearchBarDataProps = SearchBarViewDataProps & + SearchBarAutocompleteViewDataProps & + SearchBarAdvancedViewDataProps & + SearchBarBasicViewDataProps; interface SearchBarViewDataProps { searchValue: string; @@ -78,10 +66,10 @@ interface SearchBarViewDataProps { vocabulary?: Vocabulary; } -export type SearchBarActionProps = SearchBarViewActionProps - & SearchBarAutocompleteViewActionProps - & SearchBarAdvancedViewActionProps - & SearchBarBasicViewActionProps; +export type SearchBarActionProps = SearchBarViewActionProps & + SearchBarAutocompleteViewActionProps & + SearchBarAdvancedViewActionProps & + SearchBarBasicViewActionProps; interface SearchBarViewActionProps { onChange: (event: React.ChangeEvent) => void; @@ -144,9 +132,11 @@ const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => } }; -export const SearchBarView = compose(connectVocabulary, withStyles(styles))( +export const SearchBarView = compose( + connectVocabulary, + withStyles(styles) +)( class extends React.Component { - debouncedSearch = debounce(() => { this.props.onSearch(this.props.searchValue); }, 1000); @@ -154,12 +144,12 @@ export const SearchBarView = compose(connectVocabulary, withStyles(styles))( handleChange = (event: React.ChangeEvent) => { this.debouncedSearch(); this.props.onChange(event); - } + }; handleSubmit = (event: React.FormEvent) => { this.debouncedSearch.clear(); this.props.onSubmit(event); - } + }; componentWillUnmount() { this.debouncedSearch.clear(); @@ -170,14 +160,14 @@ export const SearchBarView = compose(connectVocabulary, withStyles(styles))( const { classes, isPopoverOpen } = this.props; return ( <> + {isPopoverOpen && } - {isPopoverOpen && - } - - -
+ + handleKeyDown(e, props)} startAdornment={ - + @@ -197,57 +187,69 @@ export const SearchBarView = compose(connectVocabulary, withStyles(styles))( } endAdornment={ - + handleDropdownClick(e, props)}> - } /> + } + /> -
- {isPopoverOpen && getView({ ...props })} -
-
+
{isPopoverOpen && getView({ ...props })}
+
); } - }); + } +); const getView = (props: SearchBarViewProps) => { switch (props.currentView) { case SearchView.AUTOCOMPLETE: - return ; + return ( + + ); case SearchView.ADVANCED: - return ; + return ( + + ); default: - return ; + return ( + + ); } }; -const Backdrop = withStyles<'backdrop'>(theme => ({ +const Backdrop = withStyles<"backdrop">(theme => ({ backdrop: { - position: 'fixed', + position: "fixed", top: 0, right: 0, bottom: 0, left: 0, - zIndex: theme.zIndex.modal - } -}))( - ({ classes, ...props }: WithStyles<'backdrop'> & React.HTMLProps) => -
); + zIndex: theme.zIndex.modal, + }, +}))(({ classes, ...props }: WithStyles<"backdrop"> & React.HTMLProps) => ( +
+)); -- 2.30.2