From b1639352062a0fa4413183c45c2e470e191aa290 Mon Sep 17 00:00:00 2001 From: Le Roux Bodenstein Date: Thu, 15 Aug 2024 13:19:04 +0100 Subject: [PATCH] chore(e2e): add E2E test for multiple connection errors COMPASS-8153 (#6117) * remove commented code * fix connectionStatus: either * test multiple connection errors --- .../helpers/commands/click-visible.ts | 7 +- .../helpers/commands/connect.ts | 43 ++++-- .../helpers/commands/hide-visible-toasts.ts | 48 +++++-- .../helpers/commands/wait-for-animations.ts | 5 +- .../compass-e2e-tests/helpers/selectors.ts | 3 + .../tests/connection.test.ts | 128 ++++++++++++++++-- .../tests/my-queries-tab.test.ts | 11 -- 7 files changed, 194 insertions(+), 51 deletions(-) diff --git a/packages/compass-e2e-tests/helpers/commands/click-visible.ts b/packages/compass-e2e-tests/helpers/commands/click-visible.ts index 05e80d08760..ed065b2911d 100644 --- a/packages/compass-e2e-tests/helpers/commands/click-visible.ts +++ b/packages/compass-e2e-tests/helpers/commands/click-visible.ts @@ -2,6 +2,7 @@ import type { CompassBrowser } from '../compass-browser'; import type { ChainablePromiseElement } from 'webdriverio'; interface ClickOptions { + timeout?: number; scroll?: boolean; screenshot?: string; } @@ -11,16 +12,18 @@ export async function clickVisible( selector: string | ChainablePromiseElement, options?: ClickOptions ): Promise { + const waitOptions = { timeout: options?.timeout }; + function getElement() { return typeof selector === 'string' ? browser.$(selector) : selector; } const displayElement = getElement(); - await displayElement.waitForDisplayed(); + await displayElement.waitForDisplayed(waitOptions); // Clicking a thing that's still animating is unreliable at best. - await browser.waitForAnimations(selector); + await browser.waitForAnimations(selector, waitOptions); if (options?.scroll) { const scrollElement = getElement(); diff --git a/packages/compass-e2e-tests/helpers/commands/connect.ts b/packages/compass-e2e-tests/helpers/commands/connect.ts index 975d628d460..882726b03b3 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect.ts @@ -133,16 +133,38 @@ export async function waitForConnectionResult( browser: CompassBrowser, connectionName: string, { connectionStatus = 'success', timeout }: ConnectionResultOptions = {} -) { +): Promise { const waitOptions = typeof timeout !== 'undefined' ? { timeout } : undefined; + if (TEST_MULTIPLE_CONNECTIONS) { + if (await browser.$(Selectors.SidebarFilterInput).isDisplayed()) { + // Clear the filter to make sure every connection shows + await browser.clickVisible(Selectors.SidebarFilterInput); + await browser.setValueVisible(Selectors.SidebarFilterInput, ''); + } + } + if (connectionStatus === 'either') { - // TODO(COMPASS-7600,COMPASS-8153): this doesn't support compass-web or - // multiple connections yet, but also isn't encountered yet For the rare - // cases where we don't care whether it fails or succeeds - await browser - .$(`${Selectors.DatabasesTable},${Selectors.ConnectionFormErrorMessage}`) - .waitForDisplayed(); + // For the very rare cases where we don't care whether it fails or succeeds. + // Usually because the exact result is a race condition. + if (TEST_MULTIPLE_CONNECTIONS) { + const successSelector = Selectors.Multiple.connectionItemByName( + connectionName, + { + connected: true, + } + ); + const failureSelector = Selectors.ConnectionToastErrorText; + await browser + .$(`${successSelector},${failureSelector}`) + .waitForDisplayed(waitOptions); + } else { + // TODO(COMPASS-7600): this doesn't support compass-web yet, but also + // isn't encountered yet + await browser + .$(`${Selectors.MyQueriesList},${Selectors.ConnectionFormErrorMessage}`) + .waitForDisplayed(); + } } else if (connectionStatus === 'success') { // Wait for the first meaningful thing on the screen after being connected // and assume that's a good enough indicator that we are connected to the @@ -151,13 +173,8 @@ export async function waitForConnectionResult( // In compass-web, for now, we land on the Databases tab after connecting await browser .$('[data-testid="workspace-tab-button"][data-type=Databases]') - .waitForDisplayed(); + .waitForDisplayed({ timeout }); } else if (TEST_MULTIPLE_CONNECTIONS) { - if (await browser.$(Selectors.SidebarFilterInput).isDisplayed()) { - // Clear the filter to make sure every connection shows - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, ''); - } await browser .$( Selectors.Multiple.connectionItemByName(connectionName, { diff --git a/packages/compass-e2e-tests/helpers/commands/hide-visible-toasts.ts b/packages/compass-e2e-tests/helpers/commands/hide-visible-toasts.ts index 49648e2bb54..9200d05bd8f 100644 --- a/packages/compass-e2e-tests/helpers/commands/hide-visible-toasts.ts +++ b/packages/compass-e2e-tests/helpers/commands/hide-visible-toasts.ts @@ -5,29 +5,55 @@ import Debug from 'debug'; const debug = Debug('compass-e2e-tests'); +async function isToastContainerVisible( + browser: CompassBrowser +): Promise { + const toastContainer = await browser.$(Selectors.LGToastContainer); + return await toastContainer.isDisplayed(); +} + export async function hideAllVisibleToasts( browser: CompassBrowser ): Promise { - const toastContainer = await browser.$(Selectors.LGToastContainer); - const isToastContainerVisible = await toastContainer.isDisplayed(); - if (!isToastContainerVisible) { + // If there's some race condition where something else is closing the toast at + // the same time we're trying to close the toast, then make it error out + // quickly so it can be ignored and we move on. + const waitOptions = { timeout: 2_000 }; + + // LG toasts are stacked in scroll container and we need to close them all. + if (!(await isToastContainerVisible(browser))) { return; } - // LG toasts are stacked in scroll container and we need to close them all. - const toasts = await toastContainer.$$('div'); - for (const toast of toasts) { + + const toasts = await browser.$(Selectors.LGToastContainer).$$('div'); + for (const _toast of toasts) { + // if they all went away at some point, just stop + if (!(await isToastContainerVisible(browser))) { + return; + } + + const toastTestId = await _toast.getAttribute('data-testid'); + const toastSelector = `[data-testid=${toastTestId}]`; + try { await browser.hover(Selectors.LGToastContainer); - const isToastVisible = await toast.isDisplayed(); + const isToastVisible = await browser.$(toastSelector).isDisplayed(); if (!isToastVisible) { continue; } - debug('closing toast', await toast.getAttribute('data-testid')); - await browser.clickVisible(toast.$(Selectors.LGToastCloseButton)); - await toast.waitForExist({ reverse: true }); + debug('closing toast', toastTestId); + await browser.clickVisible( + `${toastSelector} ${Selectors.LGToastCloseButton}`, + waitOptions + ); + debug('waiting for toast to go away', toastTestId); + await browser + .$(toastSelector) + .waitForExist({ ...waitOptions, reverse: true }); } catch (err) { // if the toast disappears by itself in the meantime, that's fine - continue; + debug('ignoring', err); } + debug('done closing', toastTestId); } } diff --git a/packages/compass-e2e-tests/helpers/commands/wait-for-animations.ts b/packages/compass-e2e-tests/helpers/commands/wait-for-animations.ts index 0550b46e3b4..f712dd848b3 100644 --- a/packages/compass-e2e-tests/helpers/commands/wait-for-animations.ts +++ b/packages/compass-e2e-tests/helpers/commands/wait-for-animations.ts @@ -4,7 +4,8 @@ import type { CompassBrowser } from '../compass-browser'; export async function waitForAnimations( browser: CompassBrowser, - selector: string | ChainablePromiseElement + selector: string | ChainablePromiseElement, + options?: { timeout?: number } ): Promise { function getElement() { return typeof selector === 'string' ? browser.$(selector) : selector; @@ -31,7 +32,7 @@ export async function waitForAnimations( const stopped = _.isEqual(result, previousResult); previousResult = result; return stopped; - }); + }, options); } catch (err: any) { if (err.name !== 'stale element reference') { throw err; diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 61a67d14a31..13c0f045ac8 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -235,6 +235,9 @@ export const ConnectionFormConnectionColor = '[data-testid="personalization-color-input"]'; export const ConnectionFormFavoriteCheckbox = '[data-testid="personalization-favorite-checkbox"]'; +export const connectionToastById = (connectionId: string) => { + return `[data-testid="toast-connection-status--${connectionId}"]`; +}; export const ConnectionToastErrorText = '[data-testid="connection-error-text"]'; export const ConnectionToastErrorReviewButton = '[data-testid="connection-error-review"]'; diff --git a/packages/compass-e2e-tests/tests/connection.test.ts b/packages/compass-e2e-tests/tests/connection.test.ts index 0edd492a684..61c244448e7 100644 --- a/packages/compass-e2e-tests/tests/connection.test.ts +++ b/packages/compass-e2e-tests/tests/connection.test.ts @@ -281,6 +281,12 @@ async function assertCannotCreateCollection( await createModalElement.waitForDisplayed({ reverse: true }); } +function assertNotError(result: any) { + if (typeof result === 'string' && result.includes('MongoNetworkError')) { + expect.fail(result); + } +} + /** * Connection tests */ @@ -322,10 +328,13 @@ describe('Connection string', function () { it('fails for authentication errors', async function () { skipForWeb(this, 'connect happens on the outside'); + // connect await browser.connectWithConnectionString( `mongodb://a:b@127.0.0.1:${MONGODB_TEST_SERVER_PORT}/test`, { connectionStatus: 'failure' } ); + + // check the error if (TEST_MULTIPLE_CONNECTIONS) { const toastTitle = await browser.$(Selectors.LGToastTitle).getText(); expect(toastTitle).to.equal('Authentication failed.'); @@ -949,6 +958,113 @@ describe('Connection form', function () { assertNotError(result); expect(result).to.have.property('ok', 1); }); + + it('fails for multiple authentication errors', async function () { + if (!TEST_MULTIPLE_CONNECTIONS) { + this.skip(); + } + + const connection1Name = 'error-1'; + const connection2Name = 'error-2'; + + const connections: { + state: ConnectFormState; + connectionId?: string; + connectionError: string; + toastErrorText: string; + }[] = [ + { + state: { + hosts: ['127.0.0.1:27091'], + defaultUsername: 'a', + defaultPassword: 'b', + connectionName: connection1Name, + }, + connectionError: 'Authentication failed.', + toastErrorText: `There was a problem connecting to ${connection1Name}`, + }, + { + state: { + hosts: ['127.0.0.1:16666'], + connectionName: connection2Name, + }, + connectionError: 'connect ECONNREFUSED 127.0.0.1:16666', + toastErrorText: `There was a problem connecting to ${connection2Name}`, + }, + ]; + + // connect to two connections that will both fail + // This actually connects back to back rather than truly simultaneously, but + // we can only really check that both toasts display and work anyway. + for (const connection of connections) { + await browser.connectWithConnectionForm(connection.state, { + connectionStatus: 'failure', + }); + } + + // pull the connection ids out of the sidebar + for (const connection of connections) { + if (!connection.state.connectionName) { + throw new Error('expected connectionName'); + } + connection.connectionId = await browser.getConnectionIdByName( + connection.state.connectionName + ); + } + + // the last connection to be connected's toast appears on top, so we have to + // deal with the toasts in reverse + const expectedConnections = connections.slice().reverse(); + + for (const expected of expectedConnections) { + if (!expected.connectionId) { + throw new Error('expected connectionId'); + } + + // the toast should appear + const toastSelector = Selectors.connectionToastById( + expected.connectionId + ); + await browser.$(toastSelector).waitForDisplayed(); + + // check the toast title + const toastTitle = await browser + .$(`${toastSelector} ${Selectors.LGToastTitle}`) + .getText(); + expect(toastTitle).to.equal(expected.connectionError); + + // check the toast body text + const errorMessage = await browser + .$(`${toastSelector} ${Selectors.ConnectionToastErrorText}`) + .getText(); + expect(errorMessage).to.equal(expected.toastErrorText); + + // click the review button in the toast + await browser.clickVisible( + `${toastSelector} ${Selectors.ConnectionToastErrorReviewButton}` + ); + + // the toast should go away because the action was clicked + await browser.$(toastSelector).waitForDisplayed({ reverse: true }); + + // make sure the connection form is populated with this connection + await browser.$(Selectors.ConnectionModal).waitForDisplayed(); + const errorText = await browser + .$(Selectors.ConnectionFormErrorMessage) + .getText(); + expect(errorText).to.equal(expected.connectionError); + + const state = await browser.getConnectFormState(); + expect(state.hosts).to.deep.equal(expected.state.hosts); + expect(state.connectionName).to.equal(expected.state.connectionName); + + // close the modal + await browser.clickVisible(Selectors.ConnectionModalCloseButton); + await browser + .$(Selectors.ConnectionModal) + .waitForDisplayed({ reverse: true }); + } + }); }); // eslint-disable-next-line mocha/max-top-level-suites @@ -961,12 +1077,6 @@ describe('SRV connectivity', function () { }); it('resolves SRV connection string using OS DNS APIs', async function () { - if (TEST_MULTIPLE_CONNECTIONS) { - // TODO(COMPASS-8153): we have to add support in custom commands for when - // connections fail - this.skip(); - } - const compass = await init(this.test?.fullTitle()); const browser = compass.browser; @@ -1141,9 +1251,3 @@ describe('FLE2', function () { expect(result).to.be.equal('test'); }); }); - -function assertNotError(result: any) { - if (typeof result === 'string' && result.includes('MongoNetworkError')) { - expect.fail(result); - } -} diff --git a/packages/compass-e2e-tests/tests/my-queries-tab.test.ts b/packages/compass-e2e-tests/tests/my-queries-tab.test.ts index a93a23cdf15..f9a569c1c13 100644 --- a/packages/compass-e2e-tests/tests/my-queries-tab.test.ts +++ b/packages/compass-e2e-tests/tests/my-queries-tab.test.ts @@ -158,17 +158,6 @@ describe('My Queries tab', function () { let client_1: MongoClient; let client_2: MongoClient; - /* - const connectionStrings = [DEFAULT_CONNECTION_STRING_1]; - if (TEST_MULTIPLE_CONNECTIONS) { - connectionStrings.push(DEFAULT_CONNECTION_STRING_2); - } - clients = connectionStrings.map( - (connectionString) => new MongoClient(connectionString) - ); - - await Promise.all(clients.map((client) => client.connect())); - */ before(async function () { skipForWeb(this, 'saved queries not yet available in compass-web');