diff --git a/package-lock.json b/package-lock.json index 5c416678b65..7e6ea439e38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45272,6 +45272,7 @@ "version": "1.40.0", "devDependencies": { "@electron/rebuild": "^4.0.1", + "@mongodb-js/compass-components": "^1.46.0", "@mongodb-js/compass-test-server": "^0.3.16", "@mongodb-js/connection-info": "^0.16.3", "@mongodb-js/eslint-config-compass": "^1.4.5", @@ -69047,6 +69048,7 @@ "version": "file:packages/compass-e2e-tests", "requires": { "@electron/rebuild": "^4.0.1", + "@mongodb-js/compass-components": "^1.46.0", "@mongodb-js/compass-test-server": "^0.3.16", "@mongodb-js/connection-info": "^0.16.3", "@mongodb-js/eslint-config-compass": "^1.4.5", diff --git a/packages/compass-components/src/components/leafygreen.tsx b/packages/compass-components/src/components/leafygreen.tsx index dd75a3543f9..e9474dc726a 100644 --- a/packages/compass-components/src/components/leafygreen.tsx +++ b/packages/compass-components/src/components/leafygreen.tsx @@ -92,6 +92,8 @@ import { ComboboxGroup, } from '@leafygreen-ui/combobox'; +export { getLgIds as getDrawerIds } from './drawer'; + // 2. Wrap and make any changes/workaround to leafygreen components. const Icon = ({ size, diff --git a/packages/compass-e2e-tests/helpers/commands/connect-form.ts b/packages/compass-e2e-tests/helpers/commands/connect-form.ts index 1dcd2866f63..0ea156d1a39 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect-form.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect-form.ts @@ -498,10 +498,10 @@ export async function setConnectFormState( } if (state.connectionColor) { - await browser.selectOption( - Selectors.ConnectionFormConnectionColor, - colorValueToName(state.connectionColor) - ); + await browser.selectOption({ + selectSelector: Selectors.ConnectionFormConnectionColor, + optionText: colorValueToName(state.connectionColor), + }); } if (state.connectionFavorite) { diff --git a/packages/compass-e2e-tests/helpers/commands/get-input-by-label.ts b/packages/compass-e2e-tests/helpers/commands/get-input-by-label.ts new file mode 100644 index 00000000000..53150fbde44 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/get-input-by-label.ts @@ -0,0 +1,11 @@ +import type { ChainablePromiseElement } from 'webdriverio'; +import type { CompassBrowser } from '../compass-browser'; + +export async function getInputByLabel( + browser: CompassBrowser, + label: ChainablePromiseElement +): Promise { + await label.waitForDisplayed(); + const inputId = await label.getAttribute('for'); + return browser.$(`[id="${inputId}"]`); +} diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index 376e62d5fa2..3d08f092c1e 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -65,3 +65,4 @@ export * from './switch-pipeline-mode'; export * from './read-first-document-content'; export * from './read-stage-operators'; export * from './click-confirmation-action'; +export * from './get-input-by-label'; diff --git a/packages/compass-e2e-tests/helpers/commands/save-favorite.ts b/packages/compass-e2e-tests/helpers/commands/save-favorite.ts index 3635aaafd87..4b53e0ba15c 100644 --- a/packages/compass-e2e-tests/helpers/commands/save-favorite.ts +++ b/packages/compass-e2e-tests/helpers/commands/save-favorite.ts @@ -14,7 +14,10 @@ export async function saveFavorite( Selectors.ConnectionFormConnectionName, favoriteName ); - await browser.selectOption(Selectors.ConnectionFormConnectionColor, color); + await browser.selectOption({ + selectSelector: Selectors.ConnectionFormConnectionColor, + optionText: color, + }); await browser.clickVisible(Selectors.ConnectionModalSaveButton); await browser.$(Selectors.ConnectionModal).waitForExist({ reverse: true }); diff --git a/packages/compass-e2e-tests/helpers/commands/select-option.ts b/packages/compass-e2e-tests/helpers/commands/select-option.ts index daa3dbde13c..146827771f2 100644 --- a/packages/compass-e2e-tests/helpers/commands/select-option.ts +++ b/packages/compass-e2e-tests/helpers/commands/select-option.ts @@ -1,14 +1,25 @@ +import type { ChainablePromiseElement } from 'webdriverio'; import type { CompassBrowser } from '../compass-browser'; +type SelectOptionOptions = { + selectSelector: string | ChainablePromiseElement; +} & ( + | { + optionText: string; + optionSelector?: never; + } + | { + optionSelector: string; + optionText?: never; + } +); + export async function selectOption( browser: CompassBrowser, - // selector must match an element (like a div) that contains the leafygreen - // select we want to operate on - selector: string, - optionText: string + { selectSelector, optionText, optionSelector }: SelectOptionOptions ): Promise { // click the field's button - const selectButton = browser.$(`${selector}`); + const selectButton = browser.$(selectSelector); await selectButton.waitForDisplayed(); await selectButton.click(); @@ -26,9 +37,11 @@ export async function selectOption( await selectList.waitForDisplayed(); // click the option - const optionSpan = selectList.$(`span=${optionText}`); - await optionSpan.scrollIntoView(); - await optionSpan.click(); + const option = + optionText !== undefined + ? selectList.$(`span=${optionText}`) + : selectList.$(optionSelector); + await browser.clickVisible(option); // wait for the list to go away again await selectList.waitForDisplayed({ reverse: true }); diff --git a/packages/compass-e2e-tests/helpers/compass.ts b/packages/compass-e2e-tests/helpers/compass.ts index fc32b9e963d..8aa1f32765c 100644 --- a/packages/compass-e2e-tests/helpers/compass.ts +++ b/packages/compass-e2e-tests/helpers/compass.ts @@ -1223,7 +1223,7 @@ function redact(value: string): string { continue; } - const quoted = `'${process.env[field] as string}'`; + const quoted = `'${process.env[field]}'`; // /regex/s would be ideal, but we'd have to escape the value to not be // interpreted as a regex. while (value.indexOf(quoted) !== -1) { diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index aad7505167b..5e3defa5844 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -1,3 +1,5 @@ +import { getDrawerIds } from '@mongodb-js/compass-components'; + export type WorkspaceTabSelectorOptions = { id?: string; connectionName?: string; @@ -1442,9 +1444,13 @@ export const CreateDataModelCollectionCheckbox = ( ): string => `${CreateDataModelModal} [data-testid="new-diagram-collection-checkbox-${collectionName}"]`; export const DataModelEditor = '[data-testid="diagram-editor-container"]'; +export const DataModelZoomOutButton = `${DataModelEditor} [aria-label="Minus Icon"]`; +export const DataModelZoomInButton = `${DataModelEditor} [aria-label="Plus Icon"]`; export const DataModelPreview = `${DataModelEditor} [data-testid="model-preview"]`; export const DataModelPreviewCollection = (collectionId: string) => - `${DataModelPreview} [data-nodeid="${collectionId}"]`; + `${DataModelPreview} [aria-roleDescription="node"][data-id="${collectionId}"]`; +export const DataModelPreviewRelationship = (relationshipId: string) => + `${DataModelPreview} [aria-roleDescription="edge"][data-id="${relationshipId}"]`; export const DataModelApplyEditor = `${DataModelEditor} [data-testid="apply-editor"]`; export const DataModelEditorApplyButton = `${DataModelApplyEditor} [data-testid="apply-button"]`; export const DataModelUndoButton = 'button[aria-label="Undo"]'; @@ -1466,3 +1472,29 @@ export const DataModelsListItem = (diagramName?: string) => { export const DataModelsListItemActions = (diagramName: string) => `${DataModelsListItem(diagramName)} [aria-label="Show actions"]`; export const DataModelsListItemDeleteButton = `[data-action="delete"]`; +export const DataModelAddRelationshipBtn = 'aria/Add relationship'; +export const DataModelNameInput = '//label[text()="Name"]'; +export const DataModelRelationshipLocalCollectionSelect = + '//label[text()="Local collection"]'; +export const DataModelRelationshipLocalFieldSelect = + '//label[text()="Local field"]'; +export const DataModelRelationshipLocalCardinalitySelect = + '//label[text()="Local cardinality"]'; +export const DataModelRelationshipForeignCollectionSelect = + '//label[text()="Foreign collection"]'; +export const DataModelRelationshipForeignFieldSelect = + '//label[text()="Foreign field"]'; +export const DataModelRelationshipForeignCardinalitySelect = + '//label[text()="Foreign cardinality"]'; +export const DataModelRelationshipCardinalityOption = (value: string) => + `[role="option"][value="${value}"]`; +export const DataModelCollectionRelationshipItem = (relationshipId: string) => + `li[data-relationship-id="${relationshipId}"]`; +export const DataModelCollectionRelationshipItemEdit = `[aria-label="Edit relationship"]`; +export const DataModelCollectionRelationshipItemDelete = `[aria-label="Delete relationship"]`; + +// Side drawer +export const SideDrawer = `[data-testid="${getDrawerIds().root}"]`; +export const SideDrawerCloseButton = `[data-testid="${ + getDrawerIds().closeButton +}"]`; diff --git a/packages/compass-e2e-tests/package.json b/packages/compass-e2e-tests/package.json index 09221e148ac..bb68275d4bf 100644 --- a/packages/compass-e2e-tests/package.json +++ b/packages/compass-e2e-tests/package.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@electron/rebuild": "^4.0.1", + "@mongodb-js/compass-components": "^1.46.0", "@mongodb-js/compass-test-server": "^0.3.16", "@mongodb-js/connection-info": "^0.16.3", "@mongodb-js/eslint-config-compass": "^1.4.5", diff --git a/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts b/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts index f277b3a0da9..939c4944d36 100644 --- a/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-aggregations-tab.test.ts @@ -1596,10 +1596,12 @@ describe('Collection aggregations tab', function () { 'name' ); - await browser.selectOption( - `${Selectors.AggregationWizardSortFormDirectionSelect(0)} button`, - 'Ascending' - ); + await browser.selectOption({ + selectSelector: `${Selectors.AggregationWizardSortFormDirectionSelect( + 0 + )} button`, + optionText: 'Ascending', + }); await browser.clickVisible(Selectors.AggregationWizardApplyButton); diff --git a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts index b4868020f31..f297f99bbd5 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -27,8 +27,18 @@ interface Node { position: { x: number; y: number }; } +interface Edge { + id: string; + source: string; + target: string; + markerStart: string; + markerEnd: string; + selected: boolean; +} + type DiagramInstance = { getNodes: () => Array; + getEdges: () => Array; }; async function setupDiagram( @@ -52,17 +62,17 @@ async function setupDiagram( await browser.clickVisible(Selectors.CreateDataModelConfirmButton); // Select existing connection - await browser.selectOption( - Selectors.CreateDataModelConnectionSelector, - options.connectionName - ); + await browser.selectOption({ + selectSelector: Selectors.CreateDataModelConnectionSelector, + optionText: options.connectionName, + }); await browser.clickVisible(Selectors.CreateDataModelConfirmButton); // Select a database - await browser.selectOption( - Selectors.CreateDataModelDatabaseSelector, - options.databaseName - ); + await browser.selectOption({ + selectSelector: Selectors.CreateDataModelDatabaseSelector, + optionText: options.databaseName, + }); await browser.clickVisible(Selectors.CreateDataModelConfirmButton); // Ensure that all the collections are selected by default @@ -77,6 +87,32 @@ async function setupDiagram( await dataModelEditor.waitForDisplayed(); } +async function selectCollectionOnTheDiagram( + browser: CompassBrowser, + ns: string +) { + // If the drawer is open, close it + // Otherwise the drawer or the minimap can cover the collection node + const drawer = browser.$(Selectors.SideDrawer); + if (await drawer.isDisplayed()) { + await browser.clickVisible(Selectors.SideDrawerCloseButton); + await drawer.waitForDisplayed({ reverse: true }); + } + + // Click on the collection node to open the drawer + const collectionNode = browser.$(Selectors.DataModelPreviewCollection(ns)); + await collectionNode.waitForClickable(); + + await collectionNode.click(); + + await drawer.waitForDisplayed(); + + const collectionName = await browser.getInputByLabel( + drawer.$(Selectors.DataModelNameInput) + ); + expect(await collectionName.getValue()).to.equal(ns); +} + async function getDiagramNodes( browser: CompassBrowser, expectedCount: number @@ -97,13 +133,37 @@ async function getDiagramNodes( return nodes; } +async function getDiagramEdges( + browser: CompassBrowser, + expectedCount: number +): Promise { + let edges: Edge[] = []; + await browser.waitUntil(async () => { + edges = await browser.execute(function (selector) { + const node = document.querySelector(selector); + if (!node) { + throw new Error(`Element with selector ${selector} not found`); + } + return ( + node as Element & { _diagram: DiagramInstance } + )._diagram.getEdges(); + }, Selectors.DataModelEditor); + return edges.length === expectedCount; + }); + return edges; +} + /** * Moves a node to the specified coordinates and returns its original position. */ async function moveNode( browser: CompassBrowser, selector: string, - coordinates: { x: number; y: number } + pointerActionMoveParams: { + x: number; + y: number; + origin?: 'pointer' | 'viewport'; + } ) { const node = browser.$(selector); @@ -117,9 +177,9 @@ async function moveNode( y: Math.round(startPosition.y + nodeSize.height / 2), }) .down({ button: 0 }) // Left mouse button - .move({ ...coordinates, duration: 1000, origin: 'pointer' }) + .move({ duration: 1000, origin: 'pointer', ...pointerActionMoveParams }) .pause(1000) - .move({ ...coordinates, duration: 1000, origin: 'pointer' }) + .move({ duration: 1000, origin: 'pointer', ...pointerActionMoveParams }) .up({ button: 0 }) // Release the left mouse button .perform(); await browser.waitForAnimations(node); @@ -463,4 +523,108 @@ describe('Data Modeling tab', function () { // The second one is the one we just opened expect(titles).to.include(`${dataModelName} (1)`); }); + + context('Drawer and Diagram interactions', function () { + it('allows relationship management via the sidebar', async function () { + const dataModelName = 'Test Add Relationship Manually'; + await setupDiagram(browser, { + diagramName: dataModelName, + connectionName: DEFAULT_CONNECTION_NAME_1, + databaseName: 'test', + }); + + const dataModelEditor = browser.$(Selectors.DataModelEditor); + await dataModelEditor.waitForDisplayed(); + + // There are no edges initially + await getDiagramEdges(browser, 0); + + // Click on the collection to open the drawer + await selectCollectionOnTheDiagram(browser, 'test.testCollection-one'); + + // Click the add relationship button + const drawer = browser.$(Selectors.SideDrawer); + + const addRelationshipBtn = browser.$( + Selectors.DataModelAddRelationshipBtn + ); + await addRelationshipBtn.waitForClickable(); + await addRelationshipBtn.click(); + + // Verify that the local collection is pre-selected + const localCollectionSelect = await browser.getInputByLabel( + drawer.$(Selectors.DataModelRelationshipLocalCollectionSelect) + ); + expect(await localCollectionSelect.getValue()).to.equal( + 'testCollection-one' + ); + + // Select the foreign collection + await browser.selectOption({ + selectSelector: await browser.getInputByLabel( + drawer.$(Selectors.DataModelRelationshipForeignCollectionSelect) + ), + optionText: 'testCollection-two', + }); + + // See the relationship on the diagram + const edges = await getDiagramEdges(browser, 1); + expect(edges).to.have.lengthOf(1); + expect(edges[0]).to.deep.include({ + source: 'test.testCollection-one', + target: 'test.testCollection-two', + markerStart: 'one', + markerEnd: 'one', + }); + const relationshipId = edges[0].id; + + // Select the other collection and see that the new relationship is listed + await selectCollectionOnTheDiagram(browser, 'test.testCollection-two'); + const relationshipItem = drawer.$( + Selectors.DataModelCollectionRelationshipItem(relationshipId) + ); + expect(await relationshipItem.isDisplayed()).to.be.true; + expect(await relationshipItem.getText()).to.include('testCollection-one'); + + // Edit the relationship + await relationshipItem.waitForDisplayed(); + await relationshipItem + .$(Selectors.DataModelCollectionRelationshipItemEdit) + .click(); + const relationshipName = await browser.getInputByLabel( + drawer.$(Selectors.DataModelNameInput) + ); + await relationshipName.setValue('updatedRelationshipName'); + await browser.selectOption({ + selectSelector: await browser.getInputByLabel( + drawer.$(Selectors.DataModelRelationshipForeignCardinalitySelect) + ), + optionSelector: Selectors.DataModelRelationshipCardinalityOption('100'), + }); + + // See the updated relationship on the diagram + const updatedEdges = await getDiagramEdges(browser, 1); + expect(updatedEdges).to.have.lengthOf(1); + expect(updatedEdges[0]).to.deep.include({ + source: 'test.testCollection-one', + target: 'test.testCollection-two', + markerStart: 'one', + markerEnd: 'many', + }); + + // Select the first collection again and delete the relationship + await selectCollectionOnTheDiagram(browser, 'test.testCollection-one'); + await relationshipItem.waitForDisplayed(); + expect(await relationshipItem.getText()).to.include( + 'updatedRelationshipName' + ); + await relationshipItem + .$(Selectors.DataModelCollectionRelationshipItemDelete) + .click(); + + // Verify that the relationship is removed from the list and the diagram + expect(await relationshipItem.isExisting()).to.be.false; + await getDiagramEdges(browser, 0); + }); + }); }); 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 05660f653ea..cb4561df752 100644 --- a/packages/compass-e2e-tests/tests/my-queries-tab.test.ts +++ b/packages/compass-e2e-tests/tests/my-queries-tab.test.ts @@ -273,14 +273,14 @@ describe('My Queries tab', function () { // the open item modal - select a new collection const openModal = browser.$(Selectors.OpenSavedItemModal); await openModal.waitForDisplayed(); - await browser.selectOption( - `${Selectors.OpenSavedItemDatabaseField} button`, - 'test' - ); - await browser.selectOption( - `${Selectors.OpenSavedItemCollectionField} button`, - 'numbers-renamed' - ); + await browser.selectOption({ + selectSelector: `${Selectors.OpenSavedItemDatabaseField} button`, + optionText: 'test', + }); + await browser.selectOption({ + selectSelector: `${Selectors.OpenSavedItemCollectionField} button`, + optionText: 'numbers-renamed', + }); await browser.clickVisible(Selectors.OpenSavedItemModalConfirmButton); await openModal.waitForDisplayed({ reverse: true }); @@ -401,14 +401,14 @@ describe('My Queries tab', function () { // the open item modal - select a new collection const openModal = browser.$(Selectors.OpenSavedItemModal); await openModal.waitForDisplayed(); - await browser.selectOption( - `${Selectors.OpenSavedItemDatabaseField} button`, - 'test' - ); - await browser.selectOption( - `${Selectors.OpenSavedItemCollectionField} button`, - newCollectionName - ); + await browser.selectOption({ + selectSelector: `${Selectors.OpenSavedItemDatabaseField} button`, + optionText: 'test', + }); + await browser.selectOption({ + selectSelector: `${Selectors.OpenSavedItemCollectionField} button`, + optionText: newCollectionName, + }); await browser.clickParent( '[data-testid="update-query-aggregation-checkbox"]' @@ -515,18 +515,18 @@ describe('My Queries tab', function () { // the open item modal - select a new connection, database and collection const openModal = browser.$(Selectors.OpenSavedItemModal); await openModal.waitForDisplayed(); - await browser.selectOption( - `${Selectors.OpenSavedItemConnectionField} button`, - DEFAULT_CONNECTION_NAME_2 - ); - await browser.selectOption( - `${Selectors.OpenSavedItemDatabaseField} button`, - 'test' - ); - await browser.selectOption( - `${Selectors.OpenSavedItemCollectionField} button`, - newCollectionName - ); + await browser.selectOption({ + selectSelector: `${Selectors.OpenSavedItemConnectionField} button`, + optionText: DEFAULT_CONNECTION_NAME_2, + }); + await browser.selectOption({ + selectSelector: `${Selectors.OpenSavedItemDatabaseField} button`, + optionText: 'test', + }); + await browser.selectOption({ + selectSelector: `${Selectors.OpenSavedItemCollectionField} button`, + optionText: newCollectionName, + }); await browser.clickVisible(Selectors.OpenSavedItemModalConfirmButton);