diff --git a/packages/compass-e2e-tests/helpers/commands/collection-workspaces.ts b/packages/compass-e2e-tests/helpers/commands/collection-workspaces.ts index b1536e4131e..8c0f8ba451c 100644 --- a/packages/compass-e2e-tests/helpers/commands/collection-workspaces.ts +++ b/packages/compass-e2e-tests/helpers/commands/collection-workspaces.ts @@ -21,7 +21,9 @@ async function navigateToCollection( // state of Schema, and Validation tabs without re-connecting. closeExistingTabs = true ): Promise { - const connectionId = await browser.getConnectionIdByName(connectionName); + const connectionId = await browser.pages.sidebar.getConnectionIdByName( + connectionName + ); const collectionSelector = Selectors.sidebarCollection( connectionId, @@ -34,9 +36,9 @@ async function navigateToCollection( } // search for the collection and wait for the collection to be there and visible - await browser.clickVisible(Selectors.SidebarFilterInput); + await browser.clickVisible(browser.pages.sidebar.$filterInput); await browser.setValueVisible( - Selectors.SidebarFilterInput, + browser.pages.sidebar.$filterInput, `${dbName}.${collectionName}` ); const collectionElement = browser.$(collectionSelector); @@ -71,7 +73,7 @@ export async function navigateToCollectionTab( ); // wait for the tooltip to be gone - await browser.clickVisible(Selectors.SidebarFilterInput); + await browser.clickVisible(browser.pages.sidebar.$filterInput); await browser .$(Selectors.WorkspaceTabTooltip) .waitForDisplayed({ reverse: true }); @@ -79,7 +81,7 @@ export async function navigateToCollectionTab( await navigateWithinCurrentCollectionTabs(browser, tabName); // I don't know why, but sometimes the tooltip is shown at this point again - await browser.clickVisible(Selectors.SidebarFilterInput); + await browser.clickVisible(browser.pages.sidebar.$filterInput); await browser .$(Selectors.WorkspaceTabTooltip) .waitForDisplayed({ reverse: true }); diff --git a/packages/compass-e2e-tests/helpers/commands/connect-form.ts b/packages/compass-e2e-tests/helpers/commands/connect-form.ts index 5fd66d439cd..023a75f6f95 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect-form.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect-form.ts @@ -17,7 +17,7 @@ export async function resetConnectForm(browser: CompassBrowser): Promise { } await browser.waitForOpenModal(Selectors.ConnectionModal, { reverse: true }); - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.clickVisible(browser.pages.sidebar.$newConnectionButton); const connectionTitleSelector = Selectors.ConnectionModalTitle; @@ -965,7 +965,7 @@ export async function setupDefaultConnections(browser: CompassBrowser) { try { for (const connectionInfo of DEFAULT_CONNECTIONS) { const connectionName = getConnectionTitle(connectionInfo); - if (await browser.removeConnection(connectionName)) { + if (await browser.pages.sidebar.removeConnection(connectionName)) { debug('Removing existing connection so we do not create a duplicate', { connectionName, }); diff --git a/packages/compass-e2e-tests/helpers/commands/connect.ts b/packages/compass-e2e-tests/helpers/commands/connect.ts index a04753109d1..4092610d5ff 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect.ts @@ -68,13 +68,13 @@ export async function connectWithConnectionString( // if a connection with this name already exists, remove it otherwise we'll // add a duplicate and things will get complicated fast const connectionName = connectionNameFromString(connectionStringOrName); - if (await browser.removeConnection(connectionName)) { + if (await browser.pages.sidebar.removeConnection(connectionName)) { debug('Removing existing connection so we do not create a duplicate', { connectionName, }); } - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.clickVisible(browser.pages.sidebar.$newConnectionButton); await browser.waitForOpenModal(Selectors.ConnectionModal); await browser.setValueVisible( @@ -98,7 +98,7 @@ export async function connectWithConnectionForm( // name, make sure we don't add a duplicate so that tests can always address // this new connection. if (state.connectionName) { - if (await browser.removeConnection(state.connectionName)) { + if (await browser.pages.sidebar.removeConnection(state.connectionName)) { debug('Removing existing connection so we do not create a duplicate', { connectionName: state.connectionName, }); @@ -133,36 +133,32 @@ export async function waitForConnectionResult( const waitOptions = typeof timeout !== 'undefined' ? { timeout } : undefined; if ( - (await browser.$(Selectors.SidebarFilterInput).isDisplayed()) && - (await browser - .$(Selectors.SidebarFilterInput) - .getAttribute('aria-disabled')) !== 'true' + (await browser.pages.sidebar.$filterInput.isDisplayed()) && + (await browser.pages.sidebar.$filterInput.getAttribute('aria-disabled')) !== + 'true' ) { // Clear the filter to make sure every connection shows - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, ''); + await browser.clickVisible(browser.pages.sidebar.$filterInput); + await browser.setValueVisible(browser.pages.sidebar.$filterInput, ''); } if (connectionStatus === 'either') { // For the very rare cases where we don't care whether it fails or succeeds. // Usually because the exact result is a race condition. - const successSelector = Selectors.connectionItemByName(connectionName, { - connected: true, - }); - const failureSelector = Selectors.ConnectionToastErrorText; - await browser - .$(`${successSelector},${failureSelector}`) - .waitForDisplayed(waitOptions); + await Promise.race([ + browser.pages.sidebar + .$connectionItem(connectionName, { connected: true }) + .waitForDisplayed(waitOptions), + browser + .$(Selectors.ConnectionToastErrorText) + .waitForDisplayed(waitOptions), + ]); } 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 // server - await browser - .$( - Selectors.connectionItemByName(connectionName, { - connected: true, - }) - ) + await browser.pages.sidebar + .$connectionItem(connectionName, { connected: true }) .waitForDisplayed(); } else if (connectionStatus === 'failure') { await browser @@ -175,9 +171,9 @@ export async function waitForConnectionResult( } // make sure the placeholders for databases & collections that are loading are all gone - await browser - .$(Selectors.DatabaseCollectionPlaceholder) - .waitForDisplayed({ reverse: true }); + await browser.pages.sidebar.$databaseCollectionPlaceholder.waitForDisplayed({ + reverse: true, + }); } export async function connectByName( @@ -186,19 +182,19 @@ export async function connectByName( options: ConnectionResultOptions = {} ) { // make sure the connection shows up before we try and hover over it - await browser - .$(Selectors.sidebarConnection(connectionName)) - .waitForDisplayed(); + await browser.pages.sidebar.$connection(connectionName).waitForDisplayed(); // focus the filter input so that we can be sure the window is focused and the // mouse pointer is away from the connection itself - await browser.clickVisible(Selectors.SidebarFilterInput); + await browser.clickVisible(browser.pages.sidebar.$filterInput); // hover over the connection and hope the connect button shows up - await browser.hover(Selectors.sidebarConnection(connectionName)); + await browser.hover(browser.pages.sidebar.$connection(connectionName)); // hopefully the connect button showed up on hover and we can click it - await browser.clickVisible(Selectors.sidebarConnectionButton(connectionName)); + await browser.clickVisible( + browser.pages.sidebar.$connectButton(connectionName) + ); await browser.waitForConnectionResult(connectionName, options); } diff --git a/packages/compass-e2e-tests/helpers/commands/connection-workspaces.ts b/packages/compass-e2e-tests/helpers/commands/connection-workspaces.ts index db74c874bf7..b9bd906de15 100644 --- a/packages/compass-e2e-tests/helpers/commands/connection-workspaces.ts +++ b/packages/compass-e2e-tests/helpers/commands/connection-workspaces.ts @@ -8,9 +8,11 @@ export async function navigateToConnectionTab( tabType: 'Performance' | 'Databases' ): Promise { if (tabType === 'Databases') { - await browser.clickVisible(Selectors.sidebarConnection(connectionName)); + await browser.clickVisible( + browser.pages.sidebar.$connection(connectionName) + ); } else { - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.ViewPerformanceItem ); diff --git a/packages/compass-e2e-tests/helpers/commands/disconnect.ts b/packages/compass-e2e-tests/helpers/commands/disconnect.ts index 0aa0dd39765..26ee350659e 100644 --- a/packages/compass-e2e-tests/helpers/commands/disconnect.ts +++ b/packages/compass-e2e-tests/helpers/commands/disconnect.ts @@ -13,17 +13,16 @@ async function resetForDisconnect( // Collapse all the connections so that they will all hopefully fit on screen // and therefore be rendered. - await browser.clickVisible(Selectors.CollapseConnectionsButton); + await browser.pages.sidebar.collapseAllConnections(); if ( - (await browser.$(Selectors.SidebarFilterInput).isDisplayed()) && - (await browser - .$(Selectors.SidebarFilterInput) - .getAttribute('aria-disabled')) !== 'true' + (await browser.pages.sidebar.$filterInput.isDisplayed()) && + (await browser.pages.sidebar.$filterInput.getAttribute('aria-disabled')) !== + 'true' ) { // Clear the filter to make sure every connection shows - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, ''); + await browser.clickVisible(browser.pages.sidebar.$filterInput); + await browser.setValueVisible(browser.pages.sidebar.$filterInput, ''); } if (closeToasts) { @@ -50,7 +49,8 @@ export async function disconnectAll( // The potential problem here is that the list is virtual, so it is possible // that not every connection is rendered. Collapsing them all helps a little // bit, though. - const connectionItems = browser.$$(Selectors.ConnectedConnectionItems); + const connectionItems = browser.pages.sidebar.$$connectedConnections; + for await (const connectionItem of connectionItems) { const connectionName = await connectionItem.getAttribute( 'data-connection-name' @@ -82,16 +82,12 @@ export async function disconnectByName( ) { await resetForDisconnect(browser, { closeToasts: false }); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.DisconnectConnectionItem ); - await browser - .$( - Selectors.connectionItemByName(connectionName, { - connected: false, - }) - ) + await browser.pages.sidebar + .$connectionItem(connectionName, { connected: false }) .waitForDisplayed(); } diff --git a/packages/compass-e2e-tests/helpers/commands/drop-collection-from-sidebar.ts b/packages/compass-e2e-tests/helpers/commands/drop-collection-from-sidebar.ts index d3a79cfcc1b..940d268d284 100644 --- a/packages/compass-e2e-tests/helpers/commands/drop-collection-from-sidebar.ts +++ b/packages/compass-e2e-tests/helpers/commands/drop-collection-from-sidebar.ts @@ -14,7 +14,9 @@ export async function dropCollectionFromSidebar( 'drop-collection' ); - const connectionId = await browser.getConnectionIdByName(connectionName); + const connectionId = await browser.pages.sidebar.getConnectionIdByName( + connectionName + ); const collectionSelector = Selectors.sidebarCollection( connectionId, databaseName, diff --git a/packages/compass-e2e-tests/helpers/commands/drop-database-from-sidebar.ts b/packages/compass-e2e-tests/helpers/commands/drop-database-from-sidebar.ts index f988a19c4b8..b0772a43d4c 100644 --- a/packages/compass-e2e-tests/helpers/commands/drop-database-from-sidebar.ts +++ b/packages/compass-e2e-tests/helpers/commands/drop-database-from-sidebar.ts @@ -6,11 +6,13 @@ export async function dropDatabaseFromSidebar( connectionName: string, dbName: string ): Promise { - const connectionId = await browser.getConnectionIdByName(connectionName); + const connectionId = await browser.pages.sidebar.getConnectionIdByName( + connectionName + ); // search for the database in the sidebar filter - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, dbName); + await browser.clickVisible(browser.pages.sidebar.$filterInput); + await browser.setValueVisible(browser.pages.sidebar.$filterInput, dbName); await browser .$(Selectors.sidebarDatabase(connectionId, dbName)) .waitForDisplayed(); diff --git a/packages/compass-e2e-tests/helpers/commands/hover.ts b/packages/compass-e2e-tests/helpers/commands/hover.ts index be1236dffb8..1fd658a31b8 100644 --- a/packages/compass-e2e-tests/helpers/commands/hover.ts +++ b/packages/compass-e2e-tests/helpers/commands/hover.ts @@ -1,8 +1,10 @@ +import type { ChainablePromiseElement } from 'webdriverio'; import type { CompassBrowser } from '../compass-browser.ts'; +import { resolveElement } from '../utils.ts'; export async function hover( browser: CompassBrowser, - selector: string + selector: string | ChainablePromiseElement ): Promise { - await browser.$(selector).moveTo(); + await resolveElement(browser, selector).moveTo(); } diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index ec11d081293..2ac6d649e85 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -36,8 +36,6 @@ export * from './preferences.ts'; export * from './set-env.ts'; export * from './save-favorite.ts'; export * from './save-connection-string-as-favorite.ts'; -export * from './sidebar-connection.ts'; -export * from './select-connections-menu-item.ts'; export * from './open-settings-modal.ts'; export * from './screenshot.ts'; export * from './open-shell.ts'; diff --git a/packages/compass-e2e-tests/helpers/commands/open-shell.ts b/packages/compass-e2e-tests/helpers/commands/open-shell.ts index a37ea815bbb..081030bc5c5 100644 --- a/packages/compass-e2e-tests/helpers/commands/open-shell.ts +++ b/packages/compass-e2e-tests/helpers/commands/open-shell.ts @@ -23,7 +23,7 @@ export async function openShellFromSidebar( browser: CompassBrowser, connectionName: string ): Promise { - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.OpenShellItem, false // the item is not contained in the three-dot menu diff --git a/packages/compass-e2e-tests/helpers/commands/remove-connections.ts b/packages/compass-e2e-tests/helpers/commands/remove-connections.ts index 9fef81708d4..7546e358440 100644 --- a/packages/compass-e2e-tests/helpers/commands/remove-connections.ts +++ b/packages/compass-e2e-tests/helpers/commands/remove-connections.ts @@ -6,17 +6,16 @@ async function resetForRemove(browser: CompassBrowser) { // Collapse all the connections so that they will all hopefully fit on screen // and therefore be rendered. - await browser.clickVisible(Selectors.CollapseConnectionsButton); + await browser.clickVisible(browser.pages.sidebar.$connectionsTitle); if ( - (await browser.$(Selectors.SidebarFilterInput).isDisplayed()) && - (await browser - .$(Selectors.SidebarFilterInput) - .getAttribute('aria-disabled')) !== 'true' + (await browser.pages.sidebar.$filterInput.isDisplayed()) && + (await browser.pages.sidebar.$filterInput.getAttribute('aria-disabled')) !== + 'true' ) { // Clear the filter to make sure every connection shows - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, ''); + await browser.clickVisible(browser.pages.sidebar.$filterInput); + await browser.setValueVisible(browser.pages.sidebar.$filterInput, ''); } } @@ -34,7 +33,7 @@ export async function removeAllConnections( // The potential problem here is that the list is virtual, so it is possible // that not every connection is rendered. Collapsing them all helps a little // bit, though. - const connectionItems = browser.$$(Selectors.ConnectionItems); + const connectionItems = browser.pages.sidebar.$$connections; for await (const connectionItem of connectionItems) { console.log(connectionItem); const connectionName = await connectionItem.getAttribute( @@ -55,12 +54,12 @@ export async function removeConnectionByName( ) { await resetForRemove(browser); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.RemoveConnectionItem ); - await browser - .$(Selectors.connectionItemByName(connectionName)) + await browser.pages.sidebar + .$connectionItem(connectionName) .waitForDisplayed({ reverse: true }); } diff --git a/packages/compass-e2e-tests/helpers/commands/save-connection-string-as-favorite.ts b/packages/compass-e2e-tests/helpers/commands/save-connection-string-as-favorite.ts index ea9a829b363..e3f88c8b070 100644 --- a/packages/compass-e2e-tests/helpers/commands/save-connection-string-as-favorite.ts +++ b/packages/compass-e2e-tests/helpers/commands/save-connection-string-as-favorite.ts @@ -11,7 +11,7 @@ export async function saveConnectionStringAsFavorite( color = 'Green' ): Promise { // open the connection modal so we can fill in the connection string - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.clickVisible(browser.pages.sidebar.$newConnectionButton); favoriteName ??= new UUID().toHexString(); await browser.setValueVisible( Selectors.ConnectionFormStringInput, diff --git a/packages/compass-e2e-tests/helpers/commands/select-connections-menu-item.ts b/packages/compass-e2e-tests/helpers/commands/select-connections-menu-item.ts deleted file mode 100644 index db9f8db354f..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/select-connections-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CompassBrowser } from '../compass-browser.ts'; -import * as Selectors from '../selectors.ts'; - -export async function selectConnectionsMenuItem( - browser: CompassBrowser, - itemSelector: string -) { - await browser.clickVisible(Selectors.ConnectionsMenuButton); - await browser.$(Selectors.ConnectionsMenu).waitForDisplayed(); - await browser.clickVisible(itemSelector); -} diff --git a/packages/compass-e2e-tests/helpers/commands/sidebar-collection.ts b/packages/compass-e2e-tests/helpers/commands/sidebar-collection.ts index 697d279ebfb..0a9aec92cd0 100644 --- a/packages/compass-e2e-tests/helpers/commands/sidebar-collection.ts +++ b/packages/compass-e2e-tests/helpers/commands/sidebar-collection.ts @@ -8,12 +8,14 @@ export async function selectCollectionMenuItem( collectionName: string, actionName: string ) { - const connectionId = await browser.getConnectionIdByName(connectionName); + const connectionId = await browser.pages.sidebar.getConnectionIdByName( + connectionName + ); // search for the view in the sidebar - await browser.clickVisible(Selectors.SidebarFilterInput); + await browser.clickVisible(browser.pages.sidebar.$filterInput); await browser.setValueVisible( - Selectors.SidebarFilterInput, + browser.pages.sidebar.$filterInput, `${databaseName}.${collectionName}` ); @@ -24,10 +26,10 @@ export async function selectCollectionMenuItem( ); // scroll to the collection if necessary - await browser.scrollToVirtualItem( - Selectors.SidebarNavigationTree, - collectionSelector, - 'tree' + await browser.pages.sidebar.scrollToCollection( + connectionId, + databaseName, + collectionName ); const collectionElement = browser.$(collectionSelector); diff --git a/packages/compass-e2e-tests/helpers/commands/sidebar-connection.ts b/packages/compass-e2e-tests/helpers/commands/sidebar-connection.ts deleted file mode 100644 index 3c026ebb638..00000000000 --- a/packages/compass-e2e-tests/helpers/commands/sidebar-connection.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { CompassBrowser } from '../compass-browser.ts'; -import * as Selectors from '../selectors.ts'; - -export async function getConnectionIdByName( - browser: CompassBrowser, - connectionName: string -): Promise { - const connections = browser.$$(Selectors.sidebarConnection(connectionName)); - - const numConnections = await connections.length; - - if (numConnections !== 1) { - throw new Error( - `Found ${numConnections} connections named ${connectionName}.` - ); - } - - const connectionId = await browser - .$(Selectors.sidebarConnection(connectionName)) - .getAttribute('data-connection-id'); - - if (!connectionId) { - throw new Error( - `Could not find connection id for connection ${connectionName}` - ); - } - - return connectionId; -} - -export async function selectConnection( - browser: CompassBrowser, - connectionName: string -): Promise { - await browser.selectConnectionMenuItem( - connectionName, - Selectors.EditConnectionItem - ); - - await browser.waitUntil(async () => { - const connectionTitleSelector = Selectors.ConnectionModalTitle; - - const text = await browser.$(connectionTitleSelector).getText(); - return text === connectionName; - }); -} - -export async function selectConnectionMenuItem( - browser: CompassBrowser, - connectionName: string, - itemSelector: string, - openMenu = true -) { - const selector = Selectors.sidebarConnection(connectionName); - - await browser.waitUntil(async () => { - if ( - await browser - .$(Selectors.sidebarConnectionMenuButton(connectionName)) - .isDisplayed() - ) { - return true; - } - - // It takes some time for the connections to load - await browser.$(selector).waitForDisplayed(); - - // workaround for weirdness in the ItemActionControls menu opener icon - await browser.clickVisible(Selectors.ConnectionsTitle); - - // Hover over an arbitrary other element to ensure that the second hover will - // actually be a fresh one. This otherwise breaks if this function is called - // twice in a row. - await browser.hover(Selectors.ConnectionsTitle); - - await browser.hover(selector); - return false; - }); - - // if the action lives outside of the three-dot menu, then there's no need to open the menu - if (openMenu) { - await browser.clickVisible( - Selectors.sidebarConnectionMenuButton(connectionName) - ); - await browser.$(Selectors.ConnectionMenu).waitForDisplayed(); - } - - await browser.clickVisible(itemSelector); -} - -export async function removeConnection( - browser: CompassBrowser, - connectionName: string -): Promise { - // make sure there's no filter because if the connection is not displayed then we can't remove it - if ( - (await browser.$(Selectors.SidebarFilterInput).isExisting()) && - (await browser - .$(Selectors.SidebarFilterInput) - .getAttribute('aria-disabled')) !== 'true' - ) { - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, ''); - - // wait for a connection to appear. It must because if there are no - // connections the filter field wouldn't exist in the first place - await browser.$(Selectors.SidebarTreeItems).waitForDisplayed(); - } - - const selector = Selectors.sidebarConnection(connectionName); - if (await browser.$(selector).isExisting()) { - await browser.selectConnectionMenuItem( - connectionName, - Selectors.RemoveConnectionItem - ); - await browser.$(selector).waitForExist({ reverse: true }); - return true; - } - return false; -} - -export async function hasConnectionMenuItem( - browser: CompassBrowser, - connectionName: string, - itemSelector: string, - openMenu = true -) { - const selector = Selectors.sidebarConnection(connectionName); - - await browser.waitUntil(async () => { - if ( - await browser - .$(Selectors.sidebarConnectionMenuButton(connectionName)) - .isDisplayed() - ) { - return true; - } - - // It takes some time for the connections to load - await browser.$(selector).waitForDisplayed(); - - // workaround for weirdness in the ItemActionControls menu opener icon - await browser.clickVisible(Selectors.ConnectionsTitle); - - // Hover over an arbitrary other element to ensure that the second hover will - // actually be a fresh one. This otherwise breaks if this function is called - // twice in a row. - await browser.hover(Selectors.ConnectionsTitle); - - await browser.hover(selector); - return false; - }); - - // if the action lives outside of the three-dot menu, then there's no need to open the menu - if (openMenu) { - await browser.clickVisible( - Selectors.sidebarConnectionMenuButton(connectionName) - ); - await browser.$(Selectors.ConnectionMenu).waitForDisplayed(); - } - - return await browser.$(itemSelector).isExisting(); -} diff --git a/packages/compass-e2e-tests/helpers/compass-browser.ts b/packages/compass-e2e-tests/helpers/compass-browser.ts index 3084ce81217..e3506e2a3e0 100644 --- a/packages/compass-e2e-tests/helpers/compass-browser.ts +++ b/packages/compass-e2e-tests/helpers/compass-browser.ts @@ -1,8 +1,38 @@ import type * as Commands from './commands/index.ts'; +import type * as ElementCommands from './element-commands/index.ts'; +import type { Pages } from '../pages/index.ts'; type CommandsType = typeof Commands; +type ElementCommandsType = typeof ElementCommands; + +type ElementAugmentations = { + [key in keyof ElementCommandsType]: ElementCommandsType[key] extends ( + element: WebdriverIO.Element, + ...args: infer A + ) => infer R + ? (...args: A) => R + : never; +}; + +declare global { + // WDIO declares its own types as `declare global { namespace WebdriverIO }`, + // so re-opening the namespace is the only way to declaration-merge into + // `WebdriverIO.Element`. Disable is permanent. + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace WebdriverIO { + // Body is intentionally empty: methods come from `extends + // ElementAugmentations` (mapped over `typeof ElementCommands`). A named, + // body-less interface declaration is required for declaration merging — + // mapped types can't be merged into a namespace directly. Disable is + // permanent even after ElementCommands gains entries. + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface Element extends ElementAugmentations {} + } +} export type CompassBrowser = WebdriverIO.Browser & { + pages: Pages; +} & { [key in keyof CommandsType]: CommandsType[key] extends ( browser: CompassBrowser, ...args: infer A diff --git a/packages/compass-e2e-tests/helpers/compass.ts b/packages/compass-e2e-tests/helpers/compass.ts index e59c53c7411..8605e8e37cf 100644 --- a/packages/compass-e2e-tests/helpers/compass.ts +++ b/packages/compass-e2e-tests/helpers/compass.ts @@ -16,7 +16,9 @@ import { getConnectionTitle } from '@mongodb-js/connection-info'; export * as Selectors from './selectors.ts'; export * as Commands from './commands/index.ts'; import * as Commands from './commands/index.ts'; +import * as ElementCommands from './element-commands/index.ts'; import type { CompassBrowser } from './compass-browser.ts'; +import { buildPages, type Pages } from '../pages/index.ts'; import type { LogEntry } from './telemetry.ts'; import Debug from 'debug'; import semver from 'semver'; @@ -149,6 +151,7 @@ export class Compass { needsCloseWelcomeModal: boolean; renderLogs: RenderLogEntry[]; logs: { raw: Buffer; structured: LogEntry[] }; + pages: Pages; logPath?: string; userDataPath?: string; appName?: string; @@ -170,6 +173,10 @@ export class Compass { this.needsCloseWelcomeModal = needsCloseWelcomeModal; this.logs = { raw: Buffer.from(''), structured: [] }; this.renderLogs = []; + this.pages = buildPages(this.browser, this.mode); + // Expose the pages tree on the browser so helper-command bodies (which + // receive `browser`, not `compass`) can reach into page objects. + this.browser.pages = this.pages; } async prepare() { @@ -181,6 +188,18 @@ export class Compass { }); } + for (const [k, v] of Object.entries(ElementCommands)) { + this.browser.addCommand( + k, + function (this: WebdriverIO.Element, ...args: unknown[]) { + // @ts-expect-error same as above; type system can't follow the + // dynamic mapping from index.ts back to addCommand's signature + return v(this, ...args); + }, + true + ); + } + // The waitUntil helper will continue running even if we started tests // teardown on abort. To work around that, we will override the default // method, will short circuit the wait if we aborted, and then throw the diff --git a/packages/compass-e2e-tests/helpers/element-commands/index.ts b/packages/compass-e2e-tests/helpers/element-commands/index.ts new file mode 100644 index 00000000000..e79869b1f52 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/element-commands/index.ts @@ -0,0 +1,11 @@ +// Custom element-attached commands. Each module here exports a function with +// signature `(element: WebdriverIO.Element, ...args) => Promise`. They are +// registered onto WebdriverIO.Element via +// `browser.addCommand(name, fn, /* attachToElement */ true)` in +// Compass.prepare(), and surfaced on the Element / ChainablePromiseElement +// types through the mapped-type augmentation in compass-browser.ts. +// +// Migration PRs add entries here as generic UI primitives move out of +// helpers/commands/ (browser-attached) onto elements where they chain +// naturally. See pages/README.md. +export {}; diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 083c8e37bd9..d16c593c7ac 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -250,15 +250,9 @@ export const ConenctionToastCancelConnectionButton = export const ConnectionToastErrorDebugButton = '[data-testid="connection-error-debug"]'; -// Connections sidebar -export const ConnectionsTitle = '[data-testid="connections-header"]'; -export const SidebarNewConnectionButton = '[data-action="add-new-connection"]'; -export const ConnectButton = - '[data-testid="sidebar-navigation-item-actions-connection-connect-action"]'; -export const ConnectDropdownButton = `[data-testid="lg-split_button-trigger"]`; -export const ConnectInNewWindowButton = - '[data-action="connection-connect-in-new-window"]'; -export const ConnectionMenu = '[data-testid="sidebar-navigation-item-actions"]'; +// Connections sidebar — connection-row menu items (still consumed by helpers +// scheduled for migration in later PRs; sidebar shell + connect controls moved +// to SidebarPage in PR 1.1a) export const CreateDatabaseButton = '[data-testid="sidebar-navigation-item-actions-create-database-action"]'; export const OpenShellItem = @@ -281,36 +275,11 @@ export const RefreshDatabasesItem = '[data-testid="sidebar-navigation-item-actions-refresh-databases-action"]'; export const ClusterInfoItem = '[data-testid="sidebar-navigation-item-actions-open-connection-info-action"]'; -export const ConnectionsMenuButton = - '[data-testid="connections-list-title-actions-show-actions"]'; -export const ConnectionsMenu = '[data-testid="connections-list-title-actions"]'; export const ExportConnectionsModalOpen = '[data-testid="connections-list-title-actions-export-saved-connections-action"]'; export const ImportConnectionsModalOpen = '[data-testid="connections-list-title-actions-import-saved-connections-action"]'; -export const InUseEncryptionMarker = '[data-action="open-csfle-modal"]'; - -export const ConnectionItems = - '[role="treeitem"][aria-level="1"] [data-is-connected]'; -export const ConnectedConnectionItems = - '[role="treeitem"][aria-level="1"] [data-is-connected=true]'; - -export const NoDeploymentsText = '[data-testid="no-deployments-text"]'; -export const AddNewConnectionButton = - '[data-testid="add-new-connection-button"]'; - -export const connectionItemByName = ( - connectionName: string, - { connected }: { connected?: boolean } = {} -) => { - const connectedFilter = - connected !== undefined - ? `[data-is-connected="${connected.toString()}"]` - : ''; - return `[role="treeitem"][aria-level="1"] [data-connection-name="${connectionName}"]${connectedFilter}`; -}; - // Rename Collection Modal export const RenameCollectionModal = '[data-testid="rename-collection-modal"]'; export const RenameCollectionModalInput = @@ -325,21 +294,18 @@ export const RenameCollectionModalErrorBanner = '[data-testid="rename-collection-modal-error"]'; export const RenameCollectionModalCloseButton = `${RenameCollectionModal} [aria-label="Close modal"]`; -// Database-Collection Sidebar -export const Sidebar = '[data-testid="navigation-sidebar"]'; -export const SidebarNavigationTree = '[data-testid="sidebar-navigation-tree"]'; -export const SidebarTreeItems = `${SidebarNavigationTree} [role="treeitem"]`; -export const SidebarFilterInput = '[data-testid="sidebar-filter-input"]'; -export const SidebarTitle = '[data-testid="sidebar-title"]'; +// Database-Collection Sidebar — db/collection tree (sidebar shell moved to +// SidebarPage in PR 1.1a; the tree itself migrates in PR 1.1b) +// Internal: still composed into the selectors below and into workspace-tab +// selectors. Removed in PR 1.1b alongside `sidebarDatabase` / `sidebarCollection`. +const Sidebar = '[data-testid="navigation-sidebar"]'; + export const SidebarNavigationItemShowActionsButton = '[data-testid="sidebar-navigation-item-actions-show-actions"]'; export const RenameCollectionButton = '[data-testid="sidebar-navigation-item-actions-rename-collection-action"]'; export const DropDatabaseButton = '[data-action="drop-database"]'; export const CreateCollectionButton = '[data-action="create-collection"]'; -export const DatabaseCollectionPlaceholder = '[data-testid="placeholder"]'; -export const CollapseConnectionsButton = - '[data-testid="connections-list-title-actions-collapse-all-connections-action"]'; export const sidebarDatabase = ( connectionId: string, @@ -369,33 +335,6 @@ export const sidebarCollection = ( return `${Sidebar} [data-namespace="${dbName}.${collectionName}"]`; }; -export const sidebarConnection = (connectionName: string): string => { - return `${Sidebar} [data-connection-name="${connectionName}"]`; -}; - -export const sidebarConnectionButton = (connectionName: string): string => { - return `${sidebarConnection( - connectionName - )} [data-action="connection-connect"]`; -}; - -export const sidebarConnectionDropdownButton = ( - connectionName: string -): string => { - return `${sidebarConnection(connectionName)} ${ConnectDropdownButton}`; -}; - -export const sidebarConnectionActionButton = ( - connectionName: string, - selector: string -): string => { - return `${sidebarConnection(connectionName)} ${selector}`; -}; - -export const sidebarConnectionMenuButton = (connectionName: string): string => { - return `${sidebarConnection(connectionName)} button[title="Show actions"]`; -}; - // CSFLE modal export const SetCSFLEEnabledLabel = '[id="set-csfle-enabled"]'; export const CSFLEConnectionModal = '[data-testid="csfle-connection-modal"]'; diff --git a/packages/compass-e2e-tests/pages/PLAN.md b/packages/compass-e2e-tests/pages/PLAN.md new file mode 100644 index 00000000000..d46d3d2ecce --- /dev/null +++ b/packages/compass-e2e-tests/pages/PLAN.md @@ -0,0 +1,301 @@ +# compass-e2e-tests Page Object refactor + +## Context + +`packages/compass-e2e-tests/` currently dumps test infrastructure into two flat structures: + +- `helpers/selectors.ts` — **1547 LoC, ~712 exports, ~64 parameterized selector functions**, organized only by hand-maintained `// section` comments (~42 sections). 88% of selectors already use `data-testid`, so the underlying selector strategy is sound; the structural problem is the flat file and the lack of an owner per selector. +- `helpers/commands/` — **79 files, ~4838 LoC**, each exporting `(browser, ...args) => Promise`. They are star-exported from `commands/index.ts` and then registered onto WebdriverIO via `browser.addCommand()` inside `Compass.prepare()` (`helpers/compass.ts:175-182`); TypeScript sees them through a conditional mapped type in `helpers/compass-browser.ts`. Commands are a mix of domain workflows (`connect`, `createIndex`, `setValidation`), navigation (`sidebarConnection`, `workspaceTabs`), and generic UI primitives (`clickVisible`, `hover`, `setValueVisible`, `waitForAnimations`). +- Several commands look like workarounds for older WDIO / Chrome quirks (`click-visible`, `set-value-visible`, `wait-for-animations`, `scroll-to-virtual-item`, `dialog-open-locator-strategy`) that may simplify or disappear under WebdriverIO v9. + +Tests in `tests/` (~43 files, ~18k LoC) consume both files heavily — a typical test has ~60-80 `Selectors.X` references and ~100+ `browser.customCommand()` calls. Coupling is real but mostly per-feature; cross-feature tests (documents, aggregations, import, export) are heaviest and will need splitting. + +Goal: introduce a Page Object Model rooted at `compass.pages`, fold selectors and domain commands into the page objects that own them, and revalidate the generic UI primitives against modern WebdriverIO v9 APIs (Element custom commands, implicit clickable waits, BiDi logging) to drop workarounds where possible. + +## Decisions + +| Decision | Choice | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Test access pattern** | `compass.pages..()`. Page objects are instantiated by `Compass.prepare()` and exposed via a typed `pages` tree on the `Compass` orchestrator. | +| **UI utilities** | Prefer **WDIO Element custom commands** (`browser.addCommand(name, fn, /* attachToElement */ true)`) so they chain naturally: `await $(sel).clickWhenVisible()`. Commands that are intrinsically browser-scoped (`waitForOpenModal`, `getOpenModals`, `hideAllVisibleToasts`) stay on `browser`. | +| **Selectors** | **Folded into each page object class — no separate `.selectors.ts` file.** Locators are `$`-prefixed members (getters or methods) on the page object, returning `ChainablePromiseElement`. Raw selector strings live as private `string` constants at the top of the class file or inline within each getter, never exported. The few helper commands that still take a string today get migrated to take an element (Step 7), removing the last reason to export selectors. Tests import only page objects. | +| **Naming convention** | `$identifier` for any member or method that **returns a `ChainablePromiseElement`** (`this.$insertButton`, `this.$documentRow(i)`). Action methods are camelCase verbs (`insertDocument`, `runFind`). Parameterized locators are methods, parameterless ones are getters. | +| **Migration cadence** | **Hard cutover per page.** When a page object lands, its selectors are removed from `helpers/selectors.ts` and its consuming tests migrate in the **same PR**. No long-running coexistence. PRs target ≤800 LoC; oversized features split by sub-page. | +| **Coverage** | Electron and `compass-web` migrated together per page. Atlas-cloud helpers already in `commands/atlas-cloud/` move into `pages/` under the same page objects they support. | +| **Guardrails** | Hard cutover + TypeScript errors + PR review. No ESLint guard rule — investigation showed the marginal value over hard-cutover-induced TS errors didn't justify the per-PR maintenance cost. Revisit if reintroductions start slipping through review during the long migration window. | + +## Progress + +### Step 0 — Foundation + +- [x] PR 0.1 — `pages/` skeleton (`BasePage`, `Pages` interface, `buildPages()`, `pages/README.md`, `Compass.pages` wiring, tsconfig include) +- [x] PR 0.2 — element-command registration infrastructure +- [~] PR 0.3 — ~~ESLint cutover-guard rule~~ **skipped**; built-in rules can cover it but the marginal value over hard-cutover-induced TS errors didn't justify the per-PR maintenance overhead. Revisit if reintroductions slip through review. + +### Step 1 — Shared infrastructure + +- [x] PR 1.1a — `pages/shared/sidebar/` bootstrap + connection surface +- [ ] PR 1.1b — `pages/shared/sidebar/` database + collection tree +- [ ] PR 1.2 — `pages/shared/workspace-tabs/` +- [ ] PR 1.3 — `pages/shared/modals/` + `toasts/` + +### Step 2 — Small features + +- [ ] PR 2.1 — `pages/welcome/` +- [ ] PR 2.2 — `pages/settings/` +- [ ] PR 2.3 — `pages/connection-form/` (may split) +- [ ] PR 2.4 — `pages/shell/` + +### Step 3 — Instance & database + +- [ ] PR 3.1 — `pages/instance/` +- [ ] PR 3.2 — `pages/database/` + +### Step 4 — Collection (heaviest area; split by tab) + +- [ ] PR 4.1 — `pages/collection/collection.page.ts` (header + rename + tab switcher) +- [ ] PR 4.2 — `pages/collection/documents/` (likely 2-3 PRs) +- [ ] PR 4.3 — `pages/collection/aggregations/` (likely 2 PRs) +- [ ] PR 4.4 — `pages/collection/indexes/` +- [ ] PR 4.5 — `pages/collection/schema/` + `validation/` +- [ ] PR 4.6 — `pages/collection/export/` + `import/` + +### Step 5 — Specialized features + +- [ ] PR 5.1 — `pages/my-queries/` +- [ ] PR 5.2 — `pages/data-modeling/` +- [ ] PR 5.3 — `pages/assistant/` + +### Step 6 — Cross-cutting cleanup + +- [ ] PR 6.1 — migrate remaining cross-feature tests +- [ ] PR 6.2 — delete `helpers/selectors.ts` + +### Step 7 — WebdriverIO v9 revalidation + cross-cutting element commands (parallel with Step 3+) + +- [ ] PR 7.1 — `click-visible` +- [ ] PR 7.2 — `set-value-visible` +- [ ] PR 7.3 — `wait-for-animations` +- [ ] PR 7.4 — `click-parent` +- [ ] PR 7.5 — `hover` +- [ ] PR 7.6 — `scroll-to-virtual-item` (cross-domain; element command, not Sidebar method) +- [ ] PR 7.7 — `dialog-open-locator-strategy` +- [ ] PR 7.8 — `addDebugger()` +- [ ] PR 7.9 — `expand-accordion` (reassigned from PR 1.1) +- [ ] PR 7.10 — `select-file` (reassigned from PR 4.6) +- [ ] PR 7.11 — `get-input-by-label` + `leafygreen` (single-digit usages — inline or convert) + +### Step 8 — Final shape + +- [ ] PR 8.1 — remove `helpers/commands/` star-export pattern +- [ ] PR 8.2 — trim/split `helpers/compass.ts` + +## Target architecture + +``` +packages/compass-e2e-tests/ +├── helpers/ +│ ├── compass.ts # Compass class + lifecycle (trimmed) +│ ├── compass-browser.ts # type aug (shrinks as commands move) +│ ├── element-commands/ # custom Element commands (new) +│ │ ├── index.ts # registers all element commands in prepare() +│ │ ├── click-when-visible.ts +│ │ ├── set-value-safe.ts +│ │ ├── exists-eventually.ts +│ │ ├── wait-for-aria-disabled.ts +│ │ └── … +│ ├── commands/ # browser-scoped commands only (shrinks) +│ │ ├── index.ts +│ │ ├── wait-for-open-modal.ts +│ │ ├── get-open-modals.ts +│ │ ├── hide-visible-toasts.ts +│ │ ├── screenshot.ts +│ │ └── … (small set of true browser commands) +│ └── … +├── pages/ +│ ├── README.md # conventions (naming, $ prefix, cutover rules) +│ ├── PLAN.md # this file +│ ├── base-page.ts # base class — holds `browser`, shared helpers +│ ├── index.ts # builds the pages tree; consumed by Compass +│ ├── shared/ +│ │ ├── sidebar/ +│ │ │ └── sidebar.page.ts +│ │ ├── workspace-tabs.page.ts +│ │ ├── modals.page.ts +│ │ ├── toasts.page.ts +│ │ └── codemirror.page.ts +│ ├── welcome/ +│ ├── settings/ +│ ├── connection-form/ # absorbs connect-form-state.ts types +│ ├── shell/ +│ ├── instance/ +│ ├── database/ +│ ├── collection/ +│ │ ├── collection.page.ts # header + tab switcher +│ │ ├── documents.page.ts # may split out query-bar.page.ts +│ │ ├── aggregations.page.ts # may split out stage-editor / pipeline-output +│ │ ├── schema.page.ts +│ │ ├── validation.page.ts +│ │ ├── indexes.page.ts +│ │ ├── export.page.ts +│ │ └── import.page.ts +│ ├── my-queries/ +│ ├── data-modeling/ +│ └── assistant/ +└── tests/ # tests import only `compass.pages.*` +``` + +`Compass.prepare()` gains, after current command registration: + +```ts +// register element commands (browser.addCommand(name, fn, true)) +for (const [k, v] of Object.entries(ElementCommands)) { + this.browser.addCommand( + k, + function (this: WebdriverIO.Element, ...args) { + return v(this, ...args); + }, + true + ); +} +// build pages tree +this.pages = buildPages(this.browser); +``` + +Page object shape (selectors live inline as private constants; only `$`-prefixed accessors are reachable from outside the class): + +```ts +// pages/collection/documents.page.ts +import { BasePage } from '../base-page'; + +export class DocumentsPage extends BasePage { + // Raw selector strings: private, scoped to this file, never exported. + static readonly #InsertButton = '[data-testid="insert-document-button"]'; + static readonly #documentRow = (i: number) => + `[data-testid="document-row-${i}"]`; + + // Element accessors: `$`-prefix marks anything returning a ChainablePromiseElement. + get $insertButton() { + return this.browser.$(DocumentsPage.#InsertButton); + } + $documentRow(i: number) { + return this.browser.$(DocumentsPage.#documentRow(i)); + } + + // Action methods: camelCase verbs. + async insert(doc: Record) { + await this.$insertButton.clickWhenVisible(); + // … + } +} +``` + +Inside a page object, "is this a locator or an action?" is answered by the `$` prefix at a glance. + +## Migration steps + +Each step below = one mergeable PR unless noted "may split". Hard cutover: every PR that introduces a page object also removes its selectors from `helpers/selectors.ts` (folding them inline as private members of the new page object class) and updates every consuming test. + +### Step 0 — Foundation (no test changes) + +- **PR 0.1** `pages/` skeleton: `BasePage`, `pages/index.ts` factory, `pages/README.md` (conventions: `$`-prefix, sibling selectors file, cutover rules), `Compass.pages` wiring in `helpers/compass.ts`. Add empty `pages.` slots that will be filled by later PRs. ~150 LoC. +- **PR 0.2** Element-command infrastructure: `helpers/element-commands/index.ts`, register-loop in `Compass.prepare()`, type augmentation for `WebdriverIO.Element` in `compass-browser.ts`. Ships with zero element commands — just wiring. ~80 LoC. +- ~~**PR 0.3** ESLint guard rule~~ — **skipped.** Hard cutover already produces TypeScript errors for any unmigrated usage; PR review catches reintroductions. The built-in `no-restricted-exports` + `no-restricted-syntax` rules could be scoped to `helpers/selectors.ts` and `helpers/commands/index.ts` via overrides in `packages/compass-e2e-tests/.eslintrc.js` if this turns out to be needed later; no custom plugin work required either way. + +### Step 1 — Shared infrastructure (used by every later page) + +- **PR 1.1a** `pages/shared/sidebar/sidebar.page.ts` — sidebar bootstrap + connection surface. Introduces the `SidebarPage` class, widens `pages/index.ts`'s `buildPages()` to take `(browser, mode)`, adds the first `Pages` slot (`sidebar`), and drops the now-unneeded `no-empty-object-type` disable + TODO in `pages/index.ts`. Folds the connection-list + filter + sidebar-header-menu selectors into the class. Absorbs `helpers/commands/sidebar-connection.ts` and `helpers/commands/select-connections-menu-item.ts`. Updates every test that references connection-sidebar selectors/commands. +- **PR 1.1b** `pages/shared/sidebar/` — database + collection tree extension. Extends `SidebarPage` with database/collection tree members and methods. Absorbs `helpers/commands/sidebar-collection.ts` (including `selectCollectionMenuItem`). Updates remaining tests. (Note: `sidebar-collection.ts` calls `browser.scrollToVirtualItem(...)` internally; that call stays as a browser/element command and gets revalidated in Step 7. `expand-accordion` originally listed under PR 1.1 has been reassigned — see Step 7.) +- **PR 1.2** `pages/shared/workspace-tabs/` — tab strip + `connection-workspaces`, `database-workspaces`, `collection-workspaces`, `workspace-tabs`. Removes Workspace Tabs section from `selectors.ts`. +- **PR 1.3** `pages/shared/modals/` + `pages/shared/toasts/` — modal & toast utilities. Migrates `get-open-modals`, `wait-for-open-modal`, `is-modal-open`, `is-modal-eventually-open`, `hide-visible-modal`, `hide-visible-toasts`, `click-confirmation-action`. Keeps `waitForOpenModal` as a browser command (it operates across pages); domain modal handling moves into the page object that owns each modal in later PRs. + +### Step 2 — Small features + +- **PR 2.1** `pages/welcome/` — `close-welcome-modal`. Trivial. +- **PR 2.2** `pages/settings/` — settings modal + preferences. Migrates `open-settings-modal`, `close-settings-modal`, `preferences.ts`. Updates `global-preferences.test.ts` and settings-touching tests. +- **PR 2.3** `pages/connection-form/` — connection form + advanced tabs + form-state type. Absorbs `helpers/connect-form-state.ts`, `connect-form`, `connect`, `disconnect`, `remove-connections`, `save-favorite`, `save-connection-string-as-favorite`, `navigate-to-connect-tab`. **May split** into (a) `connection-form.page.ts` (form fields + advanced tabs) and (b) `connection-actions.page.ts` (connect/disconnect/save flows + the consumer test migrations) if combined diff exceeds 800 LoC. +- **PR 2.4** `pages/shell/` — `open-shell`, `close-shell`, `shell-eval`. Updates `shell.test.ts`. + +### Step 3 — Instance & database + +- **PR 3.1** `pages/instance/` — databases list, performance tab, instance sidebar. Introduces the shared `pages/shared/drop-namespace-modal.page.ts` sub-page (the post-button-click confirmation modal flow currently in `helpers/commands/drop-namespace.ts`, used by Instance, Database, and direct tests). Migrates `add-database`, `drop-database-from-sidebar`, `drop-namespace`. `Instance.dropDatabase(name)` orchestrates `sidebar` + `dropNamespaceModal.confirm(name)`. Updates `instance-*.test.ts` and the direct callers in `instance-databases-tab.test.ts`. +- **PR 3.2** `pages/database/` — database workspace. Migrates `add-collection`, `drop-collection-from-sidebar`. Reuses the `drop-namespace-modal` sub-page introduced in PR 3.1. Updates `database-*.test.ts`. + +### Step 4 — Collection (heaviest area; split by tab) + +- **PR 4.1** `pages/collection/collection.page.ts` — header + rename + tab switcher. Updates `collection-heading.test.ts`, `collection-rename.test.ts`. +- **PR 4.2** `pages/collection/documents/` — split into `documents.page.ts` and `query-bar.page.ts` from the start to stay under 800 LoC. Migrates `run-find`, `run-find-operation`, `get-query-id`, `try-to-insert-document`, `read-first-document-content`. Updates `collection-documents-tab.test.ts`, `collection-bulk-delete.test.ts`, `collection-bulk-update.test.ts`, `collection-ai-query-mocked.test.ts`, `time-to-first-query.test.ts`, `atlas-list-collections-documents.test.ts`. Likely needs **2-3 PRs** (sub-page introductions, then test-file batches). +- **PR 4.3** `pages/collection/aggregations/` — pipeline builder, stage editor, focus mode, wizard, output, export-to-language. Migrates `select-stage-operator`, `focus-stage-operator`, `select-stage-menu-option`, `select-text-pipeline-output-option`, `select-pipeline-results-output-option`, `select-focus-mode-stage-output-option`, `save-aggregation-pipeline`, `read-stage-operators`, `toggle-aggregation-side-panel`, `switch-pipeline-mode`, `add-wizard`, `export-to-language`. Likely **2 PRs** (page objects, then test migration of the ~1000 LoC `collection-aggregations-tab.test.ts`). +- **PR 4.4** `pages/collection/indexes/` — indexes + search indexes. Migrates `create-index`, `drop-index`, `hide-index`, `unhide-index`. Updates `collection-indexes-tab.test.ts`, `search-indexes.test.ts`. +- **PR 4.5** `pages/collection/schema/` and `pages/collection/validation/`. Migrates both exports of `helpers/commands/set-validation.ts` — `setValidationWithinValidationTab` (tab-internal editor flow) and `setValidation` (high-level navigate-then-edit workflow). Updates `collection-schema-tab.test.ts`, `collection-validation-tab.test.ts`, plus the `setValidation` consumers in `collection-documents-tab.test.ts` and `collection-import.test.ts`. +- **PR 4.6** `pages/collection/export/` and `pages/collection/import/`. Migrates `set-export-filename`, `wait-for-export-to-finish` (exports `waitForExportToFinishAndCloseToast`; rename file during the move). Updates `collection-export.test.ts`, `collection-import.test.ts`. (Note: `select-file` reassigned to Step 7 since it's cross-cutting — used by data-modeling, connection import/export, collection import, and connect-form.) + +### Step 5 — Specialized features + +- **PR 5.1** `pages/my-queries/` — saved queries/aggregations. Updates `my-queries-tab.test.ts`. +- **PR 5.2** `pages/data-modeling/` — replaces XPath label selectors (the only XPath in `selectors.ts`) with `data-testid` where the product supports it; otherwise keeps the strings as private class constants on the page object. Updates `data-modeling-tab.test.ts`. +- **PR 5.3** `pages/assistant/` — AI assistant. Migrates `assistant.ts`. Updates `assistant.test.ts`, `assistant-mocked.test.ts`. + +### Step 6 — Cross-cutting cleanup + +- **PR 6.1** Migrate remaining tests not tied to a single page (`auto-connect`, `auto-update`, `connection-form`, `import-export-connections`, `in-use-encryption`, `intercom`, `logging`, `no-network-traffic`, `oidc`, `protect-connection-strings`, `proxy`, `read-only`, `read-write`, `routing`, `tabs`, `force-connection-options`, `show-kerberos-password-field`). +- **PR 6.2** Delete now-empty `helpers/selectors.ts`. Remove any remaining re-exports. + +### Step 7 — WebdriverIO v9 revalidation (runs in parallel with Step 3+ once conventions are stable) + +Each PR audits one workaround / cross-cutting helper, replaces or simplifies, and runs the full e2e suite to confirm flakiness doesn't regress. Element commands chain naturally on `$(sel)`. + +- **PR 7.1** `click-visible` (`helpers/commands/click-visible.ts:11-36`): WDIO v9 `.click()` already waits for clickable. Replace with element command `clickWhenVisible` only where the extra `waitForAnimations + scrollIntoView + screenshot` behavior is needed; replace plain usages with `$(...).click()`. Audit ~all uses across page objects. +- **PR 7.2** `set-value-visible` (`commands/set-value-visible.ts:6-29`): the retry loop with `Ctrl+A` / `Delete` predates `clearValue()`. Try `await el.clearValue(); await el.setValue(v)` first; keep `setValueSafe` as an element command only for inputs that genuinely need the retry. Audit each call site. +- **PR 7.3** `wait-for-animations` (`commands/wait-for-animations.ts:6-38`): uses location/size polling. Replace with `animationend`/`transitionend` listener via `browser.execute(...)` or BiDi `script` events on a scoped element. Likely keep as element command but with a cleaner implementation; delete redundant callers that no longer need it once `click()` waits for clickable. +- **PR 7.4** `click-parent` (`commands/click-parent.ts`): almost certainly a legacy workaround for a since-fixed DOM structure. Audit usages; rewrite each to target the actual clickable element. Delete the command. +- **PR 7.5** `hover` (`commands/hover.ts`): WDIO `moveTo()` is sufficient in v9. Replace with element command thin wrapper or inline calls. +- **PR 7.6** `scroll-to-virtual-item` (`commands/scroll-to-virtual-item.ts`, 171 LoC): the heaviest workaround. Used cross-domain (`sidebar-collection`, `database-collections-tab`, `instance-databases-tab`, `instance-sidebar`, `in-use-encryption`) so this must be a generic element command (`$list.scrollToVirtualChild(itemSelector)`), not a `SidebarPage` method. Investigate whether native `scrollIntoView({ block: 'center' })` + waiting for the virtualized child to mount is now sufficient given the virtualization library's behavior. Simplify in place; only delete if confirmed unnecessary. +- **PR 7.7** `dialog-open-locator-strategy` (`helpers/dialog-open-locator-strategy.ts:1-17`): WDIO v9 supports `aria/dialog`-style selectors. Audit usages and replace where possible; keep strategy only if a true `HTMLDialogElement.open` check has no equivalent. +- **PR 7.8** `addDebugger()` (`helpers/compass.ts` — wraps every browser method with debug logging + stack augmentation): WDIO v9 BiDi-based logging may make this redundant. Profile + audit + remove if redundant. +- **PR 7.9** `expand-accordion` (`commands/expand-accordion.ts`, 20 LoC): generic `aria-expanded` toggle pattern; only used by `connect-form.ts` today but applicable to any LG Accordion. Convert to an element command `$button.expandIfCollapsed()` (or inline at the 5 call sites if the indirection isn't worth keeping). +- **PR 7.10** `select-file` (`commands/select-file.ts`): generic `` setter. Used by data-modeling, connection import/export, collection import, and connect-form. Convert to an element command `$fileInput.attachFile(path)`. +- **PR 7.11** `get-input-by-label` (`commands/get-input-by-label.ts`, 10 LoC) and `leafygreen` (`commands/leafygreen.ts`, 22 LoC): single-digit usages. Either convert to element commands (`$label.getAssociatedInput()`, `$el.waitForLgEnabled()`) or inline at the call sites and delete the files. Decide during the PR. + +### Step 8 — Final shape + +- **PR 8.1** Remove `helpers/commands/` star-export pattern; the small set of remaining browser commands gets a flat `index.ts`. Ensure `compass-browser.ts` types reflect only what's left. +- **PR 8.2** Trim `helpers/compass.ts` (1360 LoC): split the `Compass` class (`compass.ts:144-527`) into `helpers/compass.ts`, and move standalone helpers (`init`, `cleanup`, `screenshotIfFailed`, `serverSatisfies`, `skipForWeb`, build/spawn utilities) into `helpers/test-runner-lifecycle.ts` and `helpers/test-runner-build.ts`. Not strictly required by the POM refactor — included so we leave the workspace in a coherent state. + +## Critical files + +- `packages/compass-e2e-tests/helpers/selectors.ts` — folded into the page object classes (selectors become private members on the page they belong to), then deleted. +- `packages/compass-e2e-tests/helpers/commands/index.ts` — gradually emptied. +- `packages/compass-e2e-tests/helpers/commands/*.ts` (79 files) — each migrated to either a page object method, an element command, or a slimmed-down browser command (or deleted). +- `packages/compass-e2e-tests/helpers/compass-browser.ts:5-12` — gains `WebdriverIO.Element` augmentation alongside the existing `WebdriverIO.Browser` augmentation; shrinks as commands move. +- `packages/compass-e2e-tests/helpers/compass.ts:144-215` — `Compass.prepare()` extended to register element commands and instantiate `this.pages`. +- `packages/compass-e2e-tests/helpers/connect-form-state.ts` — moves into `pages/connection-form/`. +- `packages/compass-e2e-tests/helpers/dialog-open-locator-strategy.ts` — kept, moved, or deleted per Step 7.7. +- `packages/compass-e2e-tests/helpers/commands/atlas-cloud/` — the only existing subdirectory; precedent for the migration shape (referenced in `pages/README.md`). +- `packages/compass-e2e-tests/tests/*.test.ts` (43 files) — updated alongside their page objects in the same PR. + +## Verification + +Per PR: + +1. `npm run typecheck` (workspace `compass-e2e-tests`) — proves the type augmentation and page-tree types compose. +2. `npm run lint` — proves the cutover guard rule isn't violated and no new entries were added to migrated sections. +3. `npm run test-noserver -- --mocha-grep ''` locally, both Electron and `--web`, to confirm migrated tests still pass. +4. Full CI run on the PR — catches anything missed. +5. For Step 7 revalidation PRs only: compare CI test-retry counts vs. the prior 2-3 runs on `main` to make sure simplifications didn't reintroduce flakiness. + +Done state: + +- `helpers/selectors.ts` deleted. +- `helpers/commands/` contains only true browser-scoped commands. +- `helpers/element-commands/` exists and is registered. +- Every test imports from `pages/`, never from `helpers/selectors.ts`. +- Every workaround in Step 7 either has a written justification in its source comment or has been deleted. + +## Risks + +- **Oversized PRs for heaviest test files.** `collection-documents-tab.test.ts` and `collection-aggregations-tab.test.ts` are each ~750-1000 LoC of test code and will need at least one preparatory page-object PR before the test-file rewrite PR. The migration order above flags these as multi-PR steps. +- **Element-command TypeScript augmentation in WDIO v9.** The conditional-mapped-type trick in `compass-browser.ts` doesn't compose straightforwardly with `WebdriverIO.Element`. PR 0.2 should land standalone so any type kinks surface before page objects depend on it. +- **Compass-web divergence.** Atlas-cloud / sandbox web mode has different DOM in some areas (sidebar, connection list). Each page object must account for both modes inline; consider a `mode === 'web'` branch on `BasePage` (exposing it as `this.mode`) or a sibling `*.web.page.ts` subclass for pages where divergence is large enough that branching in every getter becomes noisy. +- **Hard cutover ↔ ≤800 LoC tension.** Some features (Documents, Aggregations) cannot cutover in one PR under 800 LoC. The migration order pre-splits these by sub-page; if a PR still exceeds the budget, prefer splitting the test-file migration from the page-object introduction rather than splitting the page object itself. +- **Cutover guard skipped.** Without an automated guard, the long migration window relies on PR review to catch reintroductions of moved selectors/commands. Revisit (built-in `no-restricted-exports` + `no-restricted-syntax`, scoped via overrides) if any reintroductions actually slip through. diff --git a/packages/compass-e2e-tests/pages/README.md b/packages/compass-e2e-tests/pages/README.md new file mode 100644 index 00000000000..06062e11fb9 --- /dev/null +++ b/packages/compass-e2e-tests/pages/README.md @@ -0,0 +1,87 @@ +# pages/ + +End-to-end test page objects. + +Tests reach page objects through `compass.pages`: + +```ts +const { browser, pages } = compass; +await pages.connectionForm.connectWithString(uri); +await pages.collection.documents.insert({ a: 1 }); +``` + +`Compass.prepare()` (`helpers/compass.ts`) builds the tree via `buildPages()` from `pages/index.ts`. Each page object is a class extending `BasePage` and is registered as a slot on the `Pages` interface. + +## Conventions + +### `$`-prefix marks element accessors + +Anything returning a `ChainablePromiseElement` is prefixed with `$`. This is the only way callers can tell, at a glance, "this returns an element" vs. "this performs an action." + +Parameterless locators are getters; parameterized ones are methods. + +```ts +class DocumentsPage extends BasePage { + get $insertButton() { + return this.browser.$('[data-testid="insert-document-button"]'); + } + $documentRow(i: number) { + return this.browser.$(`[data-testid="document-row-${i}"]`); + } +} +``` + +### Selectors are folded into the page object + +Raw selector strings live as **private static class members** at the top of the page object file. They are not exported and not shared across page objects. If two pages legitimately need the same selector, refactor so one owns it and exposes a method that returns its element. + +```ts +class DocumentsPage extends BasePage { + static readonly #InsertButton = '[data-testid="insert-document-button"]'; + static readonly #documentRow = (i: number) => + `[data-testid="document-row-${i}"]`; + // … +} +``` + +### Action methods are camelCase verbs + +`insertDocument`, `runFind`, `openTab` — methods that perform an action and return `Promise`. No `$` prefix. + +### Generic UI primitives are element commands, not page methods + +`clickWhenVisible`, `setValueSafe`, `hover`, etc. chain on elements: + +```ts +await this.$insertButton.clickWhenVisible(); +await this.$nameInput.setValueSafe('foo'); +``` + +They live in `helpers/element-commands/` and are registered in `Compass.prepare()` via `browser.addCommand(name, fn, /* attachToElement */ true)`. Don't add new entries to `helpers/commands/` — that path is being retired. + +### Compass-web vs Electron + +`BasePage.mode` is `'electron' | 'web'`. Branch inside a page object when the DOM diverges: + +```ts +get $sidebarHeader() { + return this.browser.$(this.mode === 'web' ? S.WebHeader : S.ElectronHeader); +} +``` + +If divergence becomes large, split into a sibling `*.web.page.ts` that extends the electron one and overrides the divergent members. + +## Adding a new page object (the cutover rule) + +This refactor uses **hard cutover per page**. A single PR introduces a page object, folds the migrated selectors out of `helpers/selectors.ts`, removes the migrated commands from `helpers/commands/`, and updates every consuming test in the same change. No long-running coexistence. + +Steps when migrating a feature: + +1. Create `pages//.page.ts` with a class extending `BasePage`. +2. Fold the relevant selectors from `helpers/selectors.ts` into private static members on the class. +3. Convert the relevant `helpers/commands/*.ts` functions into action methods on the class — or, if they are generic UI primitives, into element commands in `helpers/element-commands/`. +4. Add the page object as a slot on the `Pages` interface and instantiate it in `buildPages()` (both in `pages/index.ts`). +5. Update every test that referenced the old selectors/commands to use the page object. +6. Delete the now-unused entries from `helpers/selectors.ts` and `helpers/commands/`. Their absence — combined with TypeScript errors at every former call site — is the cutover guard. + +PRs target ≤800 LoC. If a feature can't fit, split by sub-page (e.g., `DocumentsPage` and `QueryBarPage`) rather than by file type. diff --git a/packages/compass-e2e-tests/pages/base-page.ts b/packages/compass-e2e-tests/pages/base-page.ts new file mode 100644 index 00000000000..70f9e1d8f89 --- /dev/null +++ b/packages/compass-e2e-tests/pages/base-page.ts @@ -0,0 +1,13 @@ +import type { CompassBrowser } from '../helpers/compass-browser.ts'; + +export type CompassMode = 'electron' | 'web'; + +export abstract class BasePage { + protected readonly browser: CompassBrowser; + protected readonly mode: CompassMode; + + constructor(browser: CompassBrowser, mode: CompassMode) { + this.browser = browser; + this.mode = mode; + } +} diff --git a/packages/compass-e2e-tests/pages/index.ts b/packages/compass-e2e-tests/pages/index.ts new file mode 100644 index 00000000000..7132caee139 --- /dev/null +++ b/packages/compass-e2e-tests/pages/index.ts @@ -0,0 +1,20 @@ +import type { CompassBrowser } from '../helpers/compass-browser.ts'; +import type { CompassMode } from './base-page.ts'; +import { SidebarPage } from './shared/sidebar/sidebar.page.ts'; + +export interface Pages { + sidebar: SidebarPage; + toJSON(): string; +} + +export function buildPages(browser: CompassBrowser, mode: CompassMode): Pages { + return { + sidebar: new SidebarPage(browser, mode), + // We're assigning pages to browser and webdriver will sometimes try to + // serialize the browser object. To avoid this causing issues with circular + // references, we're just providing a simple replacement for the pages key + toJSON() { + return '[Pages]'; + }, + }; +} diff --git a/packages/compass-e2e-tests/pages/shared/sidebar/sidebar.page.ts b/packages/compass-e2e-tests/pages/shared/sidebar/sidebar.page.ts new file mode 100644 index 00000000000..96e393e794a --- /dev/null +++ b/packages/compass-e2e-tests/pages/shared/sidebar/sidebar.page.ts @@ -0,0 +1,327 @@ +import { BasePage } from '../../base-page.ts'; + +export class SidebarPage extends BasePage { + // --- private selectors (folded from helpers/selectors.ts) --- + + // Sidebar shell + static readonly #Sidebar = '[data-testid="navigation-sidebar"]'; + static readonly #SidebarTitle = '[data-testid="sidebar-title"]'; + static readonly #SidebarNavigationTree = + '[data-testid="sidebar-navigation-tree"]'; + static readonly #SidebarTreeItems = `${ + SidebarPage.#SidebarNavigationTree + } [role="treeitem"]`; + static readonly #SidebarFilterInput = '[data-testid="sidebar-filter-input"]'; + static readonly #DatabaseCollectionPlaceholder = + '[data-testid="placeholder"]'; + + // Connections list (sidebar header + per-row connect buttons) + static readonly #ConnectionsTitle = '[data-testid="connections-header"]'; + static readonly #SidebarNewConnectionButton = + '[data-action="add-new-connection"]'; + static readonly #ConnectionsMenuButton = + '[data-testid="connections-list-title-actions-show-actions"]'; + static readonly #ConnectionsMenu = + '[data-testid="connections-list-title-actions"]'; + static readonly #CollapseConnectionsButton = + '[data-testid="connections-list-title-actions-collapse-all-connections-action"]'; + static readonly #NoDeploymentsText = '[data-testid="no-deployments-text"]'; + static readonly #AddNewConnectionButton = + '[data-testid="add-new-connection-button"]'; + + // Per-connection-row menu container + connect controls + static readonly #ConnectionMenu = + '[data-testid="sidebar-navigation-item-actions"]'; + static readonly #ConnectButton = + '[data-testid="sidebar-navigation-item-actions-connection-connect-action"]'; + static readonly #ConnectDropdownButton = + '[data-testid="lg-split_button-trigger"]'; + static readonly #ConnectInNewWindowButton = + '[data-action="connection-connect-in-new-window"]'; + static readonly #InUseEncryptionMarker = '[data-action="open-csfle-modal"]'; + + // Aggregates + static readonly #ConnectionItems = + '[role="treeitem"][aria-level="1"] [data-is-connected]'; + static readonly #ConnectedConnectionItems = + '[role="treeitem"][aria-level="1"] [data-is-connected=true]'; + + // Cross-page selectors temporarily inlined: these belong to ConnectionFormPage + // (PR 2.3 will absorb them); SidebarPage references them only at the + // navigation boundary where the sidebar action opens the connection-form modal. + static readonly #EditConnectionItem = + '[data-testid="sidebar-navigation-item-actions-edit-connection-action"]'; + static readonly #RemoveConnectionItem = + '[data-testid="sidebar-navigation-item-actions-remove-connection-action"]'; + static readonly #ConnectionModalTitle = + '[data-testid="connection-form-modal"] h3'; + + // Parameterized locators (private; consumers use the $ accessors below) + static readonly #sidebarConnection = (connectionName: string) => + `${SidebarPage.#Sidebar} [data-connection-name="${connectionName}"]`; + static readonly #sidebarConnectionButton = (connectionName: string) => + `${SidebarPage.#sidebarConnection( + connectionName + )} [data-action="connection-connect"]`; + static readonly #sidebarConnectionDropdownButton = (connectionName: string) => + `${SidebarPage.#sidebarConnection(connectionName)} ${ + SidebarPage.#ConnectDropdownButton + }`; + static readonly #sidebarConnectionMenuButton = (connectionName: string) => + `${SidebarPage.#sidebarConnection( + connectionName + )} button[title="Show actions"]`; + static readonly #connectionItem = ( + connectionName: string, + { connected }: { connected?: boolean } = {} + ) => { + const connectedFilter = + connected !== undefined + ? `[data-is-connected="${connected.toString()}"]` + : ''; + return `[role="treeitem"][aria-level="1"] [data-connection-name="${connectionName}"]${connectedFilter}`; + }; + + // --- element accessors ($ = ChainablePromiseElement) --- + + get $sidebar() { + return this.browser.$(SidebarPage.#Sidebar); + } + get $sidebarTitle() { + return this.browser.$(SidebarPage.#SidebarTitle); + } + get $navigationTree() { + return this.browser.$(SidebarPage.#SidebarNavigationTree); + } + get $filterInput() { + return this.browser.$(SidebarPage.#SidebarFilterInput); + } + get $newConnectionButton() { + return this.browser.$(SidebarPage.#SidebarNewConnectionButton); + } + get $connectionsTitle() { + return this.browser.$(SidebarPage.#ConnectionsTitle); + } + get $connectionsHeaderMenuButton() { + return this.browser.$(SidebarPage.#ConnectionsMenuButton); + } + get $connectionsHeaderMenu() { + return this.browser.$(SidebarPage.#ConnectionsMenu); + } + get $connectionMenu() { + return this.browser.$(SidebarPage.#ConnectionMenu); + } + get $databaseCollectionPlaceholder() { + return this.browser.$(SidebarPage.#DatabaseCollectionPlaceholder); + } + get $noDeploymentsText() { + return this.browser.$(SidebarPage.#NoDeploymentsText); + } + get $addNewConnectionButton() { + return this.browser.$(SidebarPage.#AddNewConnectionButton); + } + + $connection(connectionName: string) { + return this.browser.$(SidebarPage.#sidebarConnection(connectionName)); + } + $connectButton(connectionName: string) { + return this.browser.$(SidebarPage.#sidebarConnectionButton(connectionName)); + } + $connectDropdownButton(connectionName: string) { + return this.browser.$( + SidebarPage.#sidebarConnectionDropdownButton(connectionName) + ); + } + $connectInNewWindowButton(connectionName: string) { + return this.$connection(connectionName).$( + SidebarPage.#ConnectInNewWindowButton + ); + } + $connectionMenuButton(connectionName: string) { + return this.browser.$( + SidebarPage.#sidebarConnectionMenuButton(connectionName) + ); + } + $connectionItem(connectionName: string, opts?: { connected?: boolean }) { + return this.browser.$(SidebarPage.#connectionItem(connectionName, opts)); + } + $connectionActionButton(connectionName: string, childSelector: string) { + return this.$connection(connectionName).$(childSelector); + } + $inUseEncryptionMarker(connectionName: string) { + return this.$connection(connectionName).$( + SidebarPage.#InUseEncryptionMarker + ); + } + + // Aggregates + get $$connections() { + return this.browser.$$(SidebarPage.#ConnectionItems); + } + get $$connectedConnections() { + return this.browser.$$(SidebarPage.#ConnectedConnectionItems); + } + get $$treeItems() { + return this.browser.$$(SidebarPage.#SidebarTreeItems); + } + + // --- action methods --- + + async getConnectionIdByName(connectionName: string): Promise { + const numConnections = await this.browser.$$( + SidebarPage.#sidebarConnection(connectionName) + ).length; + if (numConnections !== 1) { + throw new Error( + `Found ${numConnections} connections named ${connectionName}.` + ); + } + const connectionId = await this.$connection(connectionName).getAttribute( + 'data-connection-id' + ); + if (!connectionId) { + throw new Error( + `Could not find connection id for connection ${connectionName}` + ); + } + return connectionId; + } + + async selectConnection(connectionName: string): Promise { + await this.selectConnectionMenuItem( + connectionName, + SidebarPage.#EditConnectionItem + ); + await this.browser.waitUntil(async () => { + const text = await this.browser + .$(SidebarPage.#ConnectionModalTitle) + .getText(); + return text === connectionName; + }); + } + + async selectConnectionMenuItem( + connectionName: string, + itemSelector: string, + openMenu = true + ): Promise { + await this.browser.waitUntil(async () => { + if (await this.$connectionMenuButton(connectionName).isDisplayed()) { + return true; + } + // It takes some time for connections to load + await this.$connection(connectionName).waitForDisplayed(); + + // workaround for weirdness in the ItemActionControls menu opener icon + await this.browser.clickVisible(this.$connectionsTitle); + // Hover over an arbitrary other element to ensure that the second hover + // will be a fresh one, then hover the target connection. + await this.$connectionsTitle.moveTo(); + await this.$connection(connectionName).moveTo(); + return false; + }); + + if (openMenu) { + await this.browser.clickVisible( + this.$connectionMenuButton(connectionName) + ); + await this.$connectionMenu.waitForDisplayed(); + } + + await this.browser.clickVisible(itemSelector); + } + + async hasConnectionMenuItem( + connectionName: string, + itemSelector: string, + openMenu = true + ): Promise { + await this.browser.waitUntil(async () => { + if (await this.$connectionMenuButton(connectionName).isDisplayed()) { + return true; + } + await this.$connection(connectionName).waitForDisplayed(); + await this.browser.clickVisible(this.$connectionsTitle); + await this.$connectionsTitle.moveTo(); + await this.$connection(connectionName).moveTo(); + return false; + }); + + if (openMenu) { + await this.browser.clickVisible( + this.$connectionMenuButton(connectionName) + ); + await this.$connectionMenu.waitForDisplayed(); + } + + return await this.browser.$(itemSelector).isExisting(); + } + + async removeConnection(connectionName: string): Promise { + // Clear any filter so the connection row is visible to act on. + if ( + (await this.$filterInput.isExisting()) && + (await this.$filterInput.getAttribute('aria-disabled')) !== 'true' + ) { + await this.browser.clickVisible(this.$filterInput); + await this.browser.setValueVisible(this.$filterInput, ''); + // wait for a connection to appear; one must, because if there were none + // the filter field wouldn't exist in the first place + await this.browser.$(SidebarPage.#SidebarTreeItems).waitForDisplayed(); + } + + const $conn = this.$connection(connectionName); + if (await $conn.isExisting()) { + await this.selectConnectionMenuItem( + connectionName, + SidebarPage.#RemoveConnectionItem + ); + await $conn.waitForExist({ reverse: true }); + return true; + } + return false; + } + + async selectConnectionsMenuItem(itemSelector: string): Promise { + await this.browser.clickVisible(this.$connectionsHeaderMenuButton); + await this.$connectionsHeaderMenu.waitForDisplayed(); + await this.browser.clickVisible(itemSelector); + } + + async collapseAllConnections(): Promise { + await this.browser.clickVisible(SidebarPage.#CollapseConnectionsButton); + } + + // Scrolls the sidebar tree until the named collection row is rendered (the + // tree is virtualized, so off-screen items don't exist in the DOM until the + // viewport reaches them). Returns true if the row became visible. + // + // Composes the collection-row selector locally; the canonical + // `Selectors.sidebarCollection` migrates into SidebarPage in PR 1.1b. + async scrollToCollection( + connectionId: string, + dbName: string, + collectionName: string + ): Promise { + return await this.browser.scrollToVirtualItem( + SidebarPage.#SidebarNavigationTree, + SidebarPage.#sidebarCollection(connectionId, dbName, collectionName), + 'tree' + ); + } + + static readonly #sidebarCollection = ( + connectionId: string, + dbName: string, + collectionName: string + ) => { + if (connectionId) { + return `${ + SidebarPage.#Sidebar + } [data-connection-id="${connectionId}"][data-namespace="${dbName}.${collectionName}"]`; + } + return `${ + SidebarPage.#Sidebar + } [data-namespace="${dbName}.${collectionName}"]`; + }; +} diff --git a/packages/compass-e2e-tests/tests/assistant-mocked.test.ts b/packages/compass-e2e-tests/tests/assistant-mocked.test.ts index ec237c265a2..06010f40f7c 100644 --- a/packages/compass-e2e-tests/tests/assistant-mocked.test.ts +++ b/packages/compass-e2e-tests/tests/assistant-mocked.test.ts @@ -130,7 +130,7 @@ describe('MongoDB Assistant (with mocked backend)', function () { await browser.setupDefaultConnections(); await browser.connectToDefaults(); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.CreateDatabaseButton, false diff --git a/packages/compass-e2e-tests/tests/assistant.test.ts b/packages/compass-e2e-tests/tests/assistant.test.ts index cad504f9dbb..8a6181d1189 100644 --- a/packages/compass-e2e-tests/tests/assistant.test.ts +++ b/packages/compass-e2e-tests/tests/assistant.test.ts @@ -65,7 +65,7 @@ describe('MongoDB Assistant (with real backend)', function () { await browser.setupDefaultConnections(); await browser.connectToDefaults(); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.CreateDatabaseButton, false diff --git a/packages/compass-e2e-tests/tests/auto-connect.test.ts b/packages/compass-e2e-tests/tests/auto-connect.test.ts index d2dc2172d23..5255a2e38c6 100644 --- a/packages/compass-e2e-tests/tests/auto-connect.test.ts +++ b/packages/compass-e2e-tests/tests/auto-connect.test.ts @@ -28,7 +28,9 @@ async function createDefaultConnectionAndClose(name: string | undefined) { const { browser } = compass; await browser.setupDefaultConnections(); const connectionName = getDefaultConnectionNames(0); - const connectionId = await browser.getConnectionIdByName(connectionName); + const connectionId = await browser.pages.sidebar.getConnectionIdByName( + connectionName + ); if (!connectionId) { throw new Error('Expected a connection id'); } @@ -333,9 +335,8 @@ describe('Automatically connecting from the command line', function () { expect(numToasts).to.equal(0); // no active connections - const numConnectionItems = await browser.$$( - Selectors.ConnectedConnectionItems - ).length; + const numConnectionItems = await browser.pages.sidebar + .$$connectedConnections.length; expect(numConnectionItems).to.equal(0); } finally { await cleanup(compass); @@ -368,12 +369,12 @@ describe('Automatically connecting from the command line', function () { browser = compass.browser; // there should be no connection items - const numConnectionItems = await browser.$$(Selectors.ConnectionItems) + const numConnectionItems = await browser.pages.sidebar.$$connections .length; expect(numConnectionItems).to.equal(0); - await browser.$(Selectors.NoDeploymentsText).waitForDisplayed(); - await browser.$(Selectors.AddNewConnectionButton).waitForDisplayed(); + await browser.pages.sidebar.$noDeploymentsText.waitForDisplayed(); + await browser.pages.sidebar.$addNewConnectionButton.waitForDisplayed(); } finally { await cleanup(compass); } diff --git a/packages/compass-e2e-tests/tests/collection-rename.test.ts b/packages/compass-e2e-tests/tests/collection-rename.test.ts index df3cb4c4082..0d52697aa8c 100644 --- a/packages/compass-e2e-tests/tests/collection-rename.test.ts +++ b/packages/compass-e2e-tests/tests/collection-rename.test.ts @@ -94,7 +94,7 @@ describe('Collection Rename Modal', () => { await browser.disconnectAll(); await browser.connectToDefaults(); - connectionId = await browser.getConnectionIdByName( + connectionId = await browser.pages.sidebar.getConnectionIdByName( getDefaultConnectionNames(0) ); }); @@ -134,9 +134,9 @@ describe('Collection Rename Modal', () => { await renameCollectionSuccessFlow(browser, newCollectionName); // confirm that the new collection name is shown in the sidebar - await browser.clickVisible(Selectors.SidebarFilterInput); + await browser.clickVisible(browser.pages.sidebar.$filterInput); await browser.setValueVisible( - Selectors.SidebarFilterInput, + browser.pages.sidebar.$filterInput, `${databaseName}.${newCollectionName}` ); await browser diff --git a/packages/compass-e2e-tests/tests/connection-form.test.ts b/packages/compass-e2e-tests/tests/connection-form.test.ts index c7ec98bec75..22f21571c77 100644 --- a/packages/compass-e2e-tests/tests/connection-form.test.ts +++ b/packages/compass-e2e-tests/tests/connection-form.test.ts @@ -618,7 +618,7 @@ describe('Connection form', function () { if (!context.disableClipboardUsage) { // copy the connection string - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( favoriteName, Selectors.CopyConnectionStringItem ); @@ -633,7 +633,7 @@ describe('Connection form', function () { } // duplicate - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( favoriteName, Selectors.DuplicateConnectionItem ); @@ -642,23 +642,21 @@ describe('Connection form', function () { await browser.clickVisible(Selectors.ConnectionModalSaveButton); // delete the duplicate - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( `${favoriteName} (1)`, Selectors.RemoveConnectionItem ); // edit the original - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.saveFavorite(newFavoriteName, 'Pink'); // it should now be updated in the sidebar - await browser - .$(Selectors.sidebarConnection(newFavoriteName)) - .waitForDisplayed(); + await browser.pages.sidebar.$connection(newFavoriteName).waitForDisplayed(); // open the modal so we can perform some actions in there - await browser.selectConnection(newFavoriteName); + await browser.pages.sidebar.selectConnection(newFavoriteName); // the edit the connection string toggle should be on (because this is a new connection we just saved) const toggle = browser.$(Selectors.EditConnectionStringToggle); diff --git a/packages/compass-e2e-tests/tests/connection.test.ts b/packages/compass-e2e-tests/tests/connection.test.ts index cc77fb695a9..cdf483bf9ba 100644 --- a/packages/compass-e2e-tests/tests/connection.test.ts +++ b/packages/compass-e2e-tests/tests/connection.test.ts @@ -187,7 +187,7 @@ async function assertCannotCreateDb( await browser.navigateToConnectionTab(connectionName, 'Databases'); // open the create database modal from the sidebar - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.CreateDatabaseButton, false @@ -223,11 +223,13 @@ async function assertCannotCreateCollection( dbName: string, collectionName: string ): Promise { - const connectionId = await browser.getConnectionIdByName(connectionName); + const connectionId = await browser.pages.sidebar.getConnectionIdByName( + connectionName + ); // open create collection modal from the sidebar - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, dbName); + await browser.clickVisible(browser.pages.sidebar.$filterInput); + await browser.setValueVisible(browser.pages.sidebar.$filterInput, dbName); const dbElement = browser.$(Selectors.sidebarDatabase(connectionId, dbName)); await dbElement.waitForDisplayed(); await browser.hover(Selectors.sidebarDatabase(connectionId, dbName)); @@ -641,18 +643,16 @@ describe('Connect in a new window', () => { skipForWeb(this, 'connecting in new window is not supported on web'); const connectionName = getDefaultConnectionNames(0); - const connectionSelector = Selectors.sidebarConnection(connectionName); - await browser.hover(connectionSelector); + await browser.hover(browser.pages.sidebar.$connection(connectionName)); const windowsBefore = await browser.getWindowHandles(); expect(windowsBefore.length).equals(1); - const connectionElement = browser.$(connectionSelector); await browser.clickVisible( - connectionElement.$(Selectors.ConnectDropdownButton) + browser.pages.sidebar.$connectDropdownButton(connectionName) ); await browser.clickVisible( - connectionElement.$(Selectors.ConnectInNewWindowButton) + browser.pages.sidebar.$connectInNewWindowButton(connectionName) ); const windowsAfter = await browser.getWindowHandles(); @@ -667,15 +667,17 @@ describe('Connect in a new window', () => { it('shows correct connect button', async function (this) { const connectionName = getDefaultConnectionNames(0); - const connectionSelector = Selectors.sidebarConnection(connectionName); - await browser.hover(connectionSelector); - - const connectionElement = browser.$(connectionSelector); - await connectionElement.$(Selectors.ConnectButton).waitForDisplayed(); - await connectionElement.$(Selectors.ConnectDropdownButton).waitForExist({ - // TODO: Remove this as part of COMPASS-8970. - reverse: isTestingWeb(), - }); + await browser.hover(browser.pages.sidebar.$connection(connectionName)); + + await browser.pages.sidebar + .$connectButton(connectionName) + .waitForDisplayed(); + await browser.pages.sidebar + .$connectDropdownButton(connectionName) + .waitForExist({ + // TODO: Remove this as part of COMPASS-8970. + reverse: isTestingWeb(), + }); }); }); @@ -970,9 +972,10 @@ describe('Connection form', function () { if (!connection.state.connectionName) { throw new Error('expected connectionName'); } - connection.connectionId = await browser.getConnectionIdByName( - connection.state.connectionName - ); + connection.connectionId = + await browser.pages.sidebar.getConnectionIdByName( + connection.state.connectionName + ); } // the last connection to be connected's toast appears on top, so we have to diff --git a/packages/compass-e2e-tests/tests/database-collections-tab.test.ts b/packages/compass-e2e-tests/tests/database-collections-tab.test.ts index 695a6e6dbf9..507fef07417 100644 --- a/packages/compass-e2e-tests/tests/database-collections-tab.test.ts +++ b/packages/compass-e2e-tests/tests/database-collections-tab.test.ts @@ -155,7 +155,7 @@ describe('Database collections tab', function () { } // go hover somewhere else to give the next attempt a fighting chance - await browser.hover(Selectors.Sidebar); + await browser.hover(browser.pages.sidebar.$sidebar); return false; }); diff --git a/packages/compass-e2e-tests/tests/force-connection-options.test.ts b/packages/compass-e2e-tests/tests/force-connection-options.test.ts index 758802e81bd..25552546c75 100644 --- a/packages/compass-e2e-tests/tests/force-connection-options.test.ts +++ b/packages/compass-e2e-tests/tests/force-connection-options.test.ts @@ -42,7 +42,7 @@ describe('forceConnectionOptions', function () { it('forces the value of a specific connection option', async function () { // open the connection modal because that's where the warnings will be displayed - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.clickVisible(browser.pages.sidebar.$newConnectionButton); await browser.waitUntil( async () => { diff --git a/packages/compass-e2e-tests/tests/import-export-connections.test.ts b/packages/compass-e2e-tests/tests/import-export-connections.test.ts index 4db9de0c2cd..f68210b7292 100644 --- a/packages/compass-e2e-tests/tests/import-export-connections.test.ts +++ b/packages/compass-e2e-tests/tests/import-export-connections.test.ts @@ -103,7 +103,7 @@ describe('Connection Import / Export', function () { favoriteName: string, variant: (typeof variants)[number] ) { - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.clickVisible(Selectors.EditConnectionStringToggle); await browser.clickVisible(Selectors.confirmationModalConfirmButton()); const cs = await browser.getConnectFormConnectionString(true); @@ -116,7 +116,7 @@ describe('Connection Import / Export', function () { // close the modal again so connectWithConnectionString sees the expected state await browser.clickVisible(Selectors.ConnectionModalCloseButton); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( favoriteName, Selectors.RemoveConnectionItem ); @@ -148,7 +148,9 @@ describe('Connection Import / Export', function () { const { browser } = compass; // open the connection modal so we can fill in the connection string - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.clickVisible( + browser.pages.sidebar.$newConnectionButton + ); await browser.setValueVisible( Selectors.ConnectionFormStringInput, @@ -193,7 +195,7 @@ describe('Connection Import / Export', function () { ); try { const { browser } = compass; - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( favoriteName, Selectors.RemoveConnectionItem ); @@ -249,7 +251,7 @@ describe('Connection Import / Export', function () { browser = compass.browser; // open the connection modal so we can fill in the connection string - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.clickVisible(browser.pages.sidebar.$newConnectionButton); await browser.setValueVisible( Selectors.ConnectionFormStringInput, @@ -285,7 +287,7 @@ describe('Connection Import / Export', function () { // Open export modal { - await browser.selectConnectionsMenuItem( + await browser.pages.sidebar.selectConnectionsMenuItem( Selectors.ExportConnectionsModalOpen ); await browser.waitForOpenModal(Selectors.ExportConnectionsModal); @@ -348,7 +350,7 @@ describe('Connection Import / Export', function () { // Open import modal { - await browser.selectConnectionsMenuItem( + await browser.pages.sidebar.selectConnectionsMenuItem( Selectors.ImportConnectionsModalOpen ); await browser.waitForOpenModal(Selectors.ImportConnectionsModal); diff --git a/packages/compass-e2e-tests/tests/in-use-encryption.test.ts b/packages/compass-e2e-tests/tests/in-use-encryption.test.ts index fb402cca826..bc4876ef8fe 100644 --- a/packages/compass-e2e-tests/tests/in-use-encryption.test.ts +++ b/packages/compass-e2e-tests/tests/in-use-encryption.test.ts @@ -24,7 +24,7 @@ async function refresh(browser: CompassBrowser, connectionName: string) { // hit refresh, then wait for a transition to occur that will correlate to the // data actually being refreshed and arriving. - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.RefreshDatabasesItem, false @@ -199,7 +199,7 @@ describe('CSFLE / QE', function () { // in the multiple connections world, if we clicked the connection it // would connect and that's not what we want in this case. So we select // edit from the menu. - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.EditConnectionItem ); @@ -1002,10 +1002,7 @@ describe('CSFLE / QE', function () { }); await browser.clickVisible( - Selectors.sidebarConnectionActionButton( - connectionName, - Selectors.InUseEncryptionMarker - ) + browser.pages.sidebar.$inUseEncryptionMarker(connectionName) ); await browser.waitForOpenModal(Selectors.CSFLEConnectionModal); @@ -1028,10 +1025,7 @@ describe('CSFLE / QE', function () { }); await browser.clickVisible( - Selectors.sidebarConnectionActionButton( - connectionName, - Selectors.InUseEncryptionMarker - ) + browser.pages.sidebar.$inUseEncryptionMarker(connectionName) ); await browser.waitForOpenModal(Selectors.CSFLEConnectionModal); diff --git a/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts b/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts index 63c31e40150..7bab014fcba 100644 --- a/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts +++ b/packages/compass-e2e-tests/tests/instance-databases-tab.test.ts @@ -123,7 +123,7 @@ describe('Instance databases tab', function () { } // go hover somewhere else to give the next attempt a fighting chance - await browser.hover(Selectors.Sidebar); + await browser.hover(browser.pages.sidebar.$sidebar); return false; }); diff --git a/packages/compass-e2e-tests/tests/instance-sidebar.test.ts b/packages/compass-e2e-tests/tests/instance-sidebar.test.ts index af5e019633f..ff1cd202a5f 100644 --- a/packages/compass-e2e-tests/tests/instance-sidebar.test.ts +++ b/packages/compass-e2e-tests/tests/instance-sidebar.test.ts @@ -30,7 +30,7 @@ describe('Instance sidebar', function () { await createNumbersCollection(); await browser.disconnectAll(); await browser.connectToDefaults(); - connectionId = await browser.getConnectionIdByName( + connectionId = await browser.pages.sidebar.getConnectionIdByName( getDefaultConnectionNames(0) ); }); @@ -46,7 +46,7 @@ describe('Instance sidebar', function () { it('has a connection info modal with connection info', async function () { skipForWeb(this, "these actions don't exist in compass-web"); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.ClusterInfoItem ); @@ -70,45 +70,45 @@ describe('Instance sidebar', function () { Selectors.sidebarDatabaseToggle(connectionId, dbName) ); - const collectionSelector = Selectors.sidebarCollection( + await browser.pages.sidebar.scrollToCollection( connectionId, dbName, collectionName ); - await browser.scrollToVirtualItem( - Selectors.SidebarNavigationTree, - collectionSelector, - 'tree' + const collectionElement = browser.$( + Selectors.sidebarCollection(connectionId, dbName, collectionName) ); - const collectionElement = browser.$(collectionSelector); await collectionElement.waitForDisplayed(); }); it('can search for a collection', async function () { // wait for something to appear so we can be sure that things went away vs just not appearing yet await browser.waitUntil(async () => { - const numTreeItems = await browser.$$(Selectors.SidebarTreeItems).length; + const numTreeItems = await browser.pages.sidebar.$$treeItems.length; return numTreeItems > 0; }); // search for something that cannot be found to get the results to a known empty state - await browser.clickVisible(Selectors.SidebarFilterInput); + await browser.clickVisible(browser.pages.sidebar.$filterInput); await browser.setValueVisible( - Selectors.SidebarFilterInput, + browser.pages.sidebar.$filterInput, 'this does not exist' ); // make sure there's nothing visible await browser.waitUntil(async () => { - const numTreeItems = await browser.$$(Selectors.SidebarTreeItems).length; + const numTreeItems = await browser.pages.sidebar.$$treeItems.length; return numTreeItems === 0; }); // now search for something specific - await browser.setValueVisible(Selectors.SidebarFilterInput, 'numbers'); + await browser.setValueVisible( + browser.pages.sidebar.$filterInput, + 'numbers' + ); await browser.waitUntil(async () => { - const numTreeItems = await browser.$$(Selectors.SidebarTreeItems).length; + const numTreeItems = await browser.pages.sidebar.$$treeItems.length; // connection, database, collection (twice because there are two // connections) return numTreeItems === 6; @@ -119,21 +119,18 @@ describe('Instance sidebar', function () { ); expect(await dbElement.isDisplayed()).to.be.true; - const collectionSelector = Selectors.sidebarCollection( + await browser.pages.sidebar.scrollToCollection( connectionId, 'test', 'numbers' ); - await browser.scrollToVirtualItem( - Selectors.SidebarNavigationTree, - collectionSelector, - 'tree' - ); - const collectionElement = browser.$(collectionSelector); + const collectionElement = browser.$( + Selectors.sidebarCollection(connectionId, 'test', 'numbers') + ); expect(await collectionElement.isDisplayed()).to.be.true; - await browser.setValueVisible(Selectors.SidebarFilterInput, '*'); + await browser.setValueVisible(browser.pages.sidebar.$filterInput, '*'); // wait for something that didn't match the previous search to show up to make sure that it reset // (otherwise future tests will fail because the new dbs/collections won't match the filter) @@ -159,7 +156,7 @@ describe('Instance sidebar', function () { ); // open the create database modal from the sidebar - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.CreateDatabaseButton, false @@ -189,8 +186,8 @@ describe('Instance sidebar', function () { const dbName = 'test'; // existing db const collectionName = 'my-sidebar-collection'; - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, dbName); + await browser.clickVisible(browser.pages.sidebar.$filterInput); + await browser.setValueVisible(browser.pages.sidebar.$filterInput, dbName); const dbElement = browser.$( Selectors.sidebarDatabase(connectionId, dbName) @@ -238,7 +235,7 @@ describe('Instance sidebar', function () { await mongoClient.close(); } - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.RefreshDatabasesItem, false 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 cb968eeef2b..81915e48a0b 100644 --- a/packages/compass-e2e-tests/tests/my-queries-tab.test.ts +++ b/packages/compass-e2e-tests/tests/my-queries-tab.test.ts @@ -33,7 +33,7 @@ async function openMenuForQueryItem( } // go hover somewhere else to give the next attempt a fighting chance - await browser.hover(Selectors.SidebarTitle); + await browser.hover(browser.pages.sidebar.$sidebarTitle); return false; }); @@ -256,7 +256,7 @@ describe('My Queries tab', function () { .db('test') .renameCollection('numbers', 'numbers-renamed'); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.RefreshDatabasesItem, false @@ -390,7 +390,7 @@ describe('My Queries tab', function () { .db('test') .renameCollection('numbers', newCollectionName); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.RefreshDatabasesItem, false @@ -452,7 +452,7 @@ describe('My Queries tab', function () { await client_1.db('test').dropCollection('numbers'); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.RefreshDatabasesItem, false @@ -499,12 +499,12 @@ describe('My Queries tab', function () { .db('test') .renameCollection('numbers', newCollectionName); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.RefreshDatabasesItem, false ); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(1), Selectors.RefreshDatabasesItem, false @@ -564,12 +564,12 @@ describe('My Queries tab', function () { favoriteQueryName ); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(0), Selectors.RefreshDatabasesItem, false ); - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( getDefaultConnectionNames(1), Selectors.RefreshDatabasesItem, false @@ -583,7 +583,7 @@ describe('My Queries tab', function () { // the open item modal - select a new connection, database and collection await browser.waitForOpenModal(Selectors.SelectConnectionModal); - const connectionId = await browser.getConnectionIdByName( + const connectionId = await browser.pages.sidebar.getConnectionIdByName( getDefaultConnectionNames(1) ); if (!connectionId) { diff --git a/packages/compass-e2e-tests/tests/oidc.test.ts b/packages/compass-e2e-tests/tests/oidc.test.ts index 5a0ceebe5a6..f8485d23132 100644 --- a/packages/compass-e2e-tests/tests/oidc.test.ts +++ b/packages/compass-e2e-tests/tests/oidc.test.ts @@ -259,8 +259,8 @@ describe('OIDC integration', function () { } }; - await browser.removeConnection(connectionName); - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.pages.sidebar.removeConnection(connectionName); + await browser.clickVisible(browser.pages.sidebar.$newConnectionButton); await browser.waitForOpenModal(Selectors.ConnectionModal); await browser.setValueVisible( @@ -318,8 +318,8 @@ describe('OIDC integration', function () { }; }; - await browser.removeConnection(connectionName); - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.pages.sidebar.removeConnection(connectionName); + await browser.clickVisible(browser.pages.sidebar.$newConnectionButton); await browser.waitForOpenModal(Selectors.ConnectionModal); await browser.setValueVisible( Selectors.ConnectionFormStringInput, @@ -333,7 +333,7 @@ describe('OIDC integration', function () { // we have to browse somewhere that will fire off commands that require // authentication so that those commands get rejected due to the expired // auth and then that will trigger the confirmation modal we expect. - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.OpenShellItem, false @@ -363,8 +363,8 @@ describe('OIDC integration', function () { }; }; - await browser.removeConnection(connectionName); - await browser.clickVisible(Selectors.SidebarNewConnectionButton); + await browser.pages.sidebar.removeConnection(connectionName); + await browser.clickVisible(browser.pages.sidebar.$newConnectionButton); await browser.waitForOpenModal(Selectors.ConnectionModal); await browser.setValueVisible( Selectors.ConnectionFormStringInput, @@ -378,7 +378,7 @@ describe('OIDC integration', function () { // we have to browse somewhere that will fire off commands that require // authentication so that those commands get rejected due to the expired // auth and then that will trigger the confirmation modal we expect - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( connectionName, Selectors.OpenShellItem, false @@ -405,11 +405,11 @@ describe('OIDC integration', function () { connectionString ); - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.doConnect(favoriteName); await browser.disconnectAll(); - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.doConnect(favoriteName); await browser.disconnectAll(); @@ -430,7 +430,7 @@ describe('OIDC integration', function () { await browser.screenshot(`after-creating-favourite-${favoriteName}.png`); - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.doConnect(favoriteName); await browser.disconnectAll(); @@ -439,7 +439,7 @@ describe('OIDC integration', function () { ); // TODO(COMPASS-7810): when clicking on the favourite the element is somehow stale and then webdriverio throws - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.doConnect(favoriteName); await browser.disconnectAll(); @@ -458,7 +458,7 @@ describe('OIDC integration', function () { connectionString ); - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.doConnect(favoriteName); await browser.disconnectAll(); @@ -474,7 +474,7 @@ describe('OIDC integration', function () { browser = compass.browser; } - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.doConnect(favoriteName); await browser.disconnectAll(); diff --git a/packages/compass-e2e-tests/tests/protect-connection-strings.test.ts b/packages/compass-e2e-tests/tests/protect-connection-strings.test.ts index 0b7d3026ae6..0b95a3ad3f2 100644 --- a/packages/compass-e2e-tests/tests/protect-connection-strings.test.ts +++ b/packages/compass-e2e-tests/tests/protect-connection-strings.test.ts @@ -18,7 +18,7 @@ async function expectCopyConnectionStringToClipboard( expected: string ): Promise { if (!context.disableClipboardUsage) { - await browser.selectConnectionMenuItem( + await browser.pages.sidebar.selectConnectionMenuItem( favoriteName, Selectors.CopyConnectionStringItem ); @@ -79,7 +79,7 @@ describe('protectConnectionStrings', function () { }; await browser.setConnectFormState(state); await browser.saveFavorite(favoriteName, 'Yellow'); - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); expect(await browser.getConnectFormConnectionString()).to.equal( 'mongodb://foo:*****@localhost:12345/' @@ -105,7 +105,7 @@ describe('protectConnectionStrings', function () { 'mongodb://foo:bar@localhost:12345/' ); - await browser.selectConnection(favoriteName); + await browser.pages.sidebar.selectConnection(favoriteName); await browser.setFeature('protectConnectionStrings', true); diff --git a/packages/compass-e2e-tests/tests/read-only.test.ts b/packages/compass-e2e-tests/tests/read-only.test.ts index e4e21f5a85f..fb0c689c976 100644 --- a/packages/compass-e2e-tests/tests/read-only.test.ts +++ b/packages/compass-e2e-tests/tests/read-only.test.ts @@ -82,7 +82,7 @@ describe('readOnly: true / Read-Only Edition', function () { ); expect( - await browser.hasConnectionMenuItem( + await browser.pages.sidebar.hasConnectionMenuItem( getDefaultConnectionNames(0), Selectors.CreateDatabaseButton, false @@ -102,7 +102,7 @@ describe('readOnly: true / Read-Only Edition', function () { ); expect( - await browser.hasConnectionMenuItem( + await browser.pages.sidebar.hasConnectionMenuItem( getDefaultConnectionNames(0), Selectors.CreateDatabaseButton, false @@ -119,13 +119,13 @@ describe('readOnly: true / Read-Only Edition', function () { await createNumbersCollection(); await browser.connectToDefaults(); - const connectionId = await browser.getConnectionIdByName( + const connectionId = await browser.pages.sidebar.getConnectionIdByName( getDefaultConnectionNames(0) ); const dbName = 'test'; // existing db - await browser.clickVisible(Selectors.SidebarFilterInput); - await browser.setValueVisible(Selectors.SidebarFilterInput, dbName); + await browser.clickVisible(browser.pages.sidebar.$filterInput); + await browser.setValueVisible(browser.pages.sidebar.$filterInput, dbName); const dbElement = browser.$( Selectors.sidebarDatabase(connectionId, dbName) ); diff --git a/packages/compass-e2e-tests/tests/read-write.test.ts b/packages/compass-e2e-tests/tests/read-write.test.ts index 762a18bc347..0ad2f0373a0 100644 --- a/packages/compass-e2e-tests/tests/read-write.test.ts +++ b/packages/compass-e2e-tests/tests/read-write.test.ts @@ -22,7 +22,9 @@ describe('readWrite: true', function () { await browser.setupDefaultConnections(); await createNumbersCollection('numbers', 1000, true); await browser.connectToDefaults(); - connId = await browser.getConnectionIdByName(getDefaultConnectionNames(0)); + connId = await browser.pages.sidebar.getConnectionIdByName( + getDefaultConnectionNames(0) + ); }); afterEach(async function () { diff --git a/packages/compass-e2e-tests/tests/shell.test.ts b/packages/compass-e2e-tests/tests/shell.test.ts index 8dd87fd9bff..6179d6a768e 100644 --- a/packages/compass-e2e-tests/tests/shell.test.ts +++ b/packages/compass-e2e-tests/tests/shell.test.ts @@ -119,7 +119,7 @@ describe('Shell', function () { await browser.connectToDefaults(); expect( - await browser.hasConnectionMenuItem( + await browser.pages.sidebar.hasConnectionMenuItem( getDefaultConnectionNames(0), Selectors.OpenShellItem ) @@ -135,7 +135,7 @@ describe('Shell', function () { await browser.waitForOpenModal(Selectors.SettingsModal, { reverse: true }); expect( - await browser.hasConnectionMenuItem( + await browser.pages.sidebar.hasConnectionMenuItem( getDefaultConnectionNames(0), Selectors.OpenShellItem ) diff --git a/packages/compass-e2e-tests/tsconfig.json b/packages/compass-e2e-tests/tsconfig.json index ea7ef76f5eb..145f396b09a 100644 --- a/packages/compass-e2e-tests/tsconfig.json +++ b/packages/compass-e2e-tests/tsconfig.json @@ -11,5 +11,5 @@ "moduleResolution": "bundler", "allowImportingTsExtensions": true }, - "include": ["helpers", "installers", "tests", "*.ts", "index.ts"] + "include": ["helpers", "installers", "pages", "tests", "*.ts", "index.ts"] }