From 169cf7a595bb2c2d356497af4e0cd76e483b9213 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 14 Oct 2025 15:28:28 +0200 Subject: [PATCH 1/7] feat: DM inline field name edit COMPASS-9798 --- .../src/components/diagram-editor.tsx | 10 ++++ .../src/utils/nodes-and-edges.spec.ts | 50 ++++++++++++++++++- .../src/utils/nodes-and-edges.ts | 3 +- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 0d187f8905e..5009c350d93 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -22,6 +22,7 @@ import { deleteCollection, deleteRelationship, removeField, + renameField, } from '../store/diagram'; import type { EdgeProps, @@ -143,6 +144,11 @@ const DiagramContent: React.FunctionComponent<{ onCollectionSelect: (namespace: string) => void; onRelationshipSelect: (rId: string) => void; onFieldSelect: (namespace: string, fieldPath: FieldPath) => void; + onRenameField: ( + namespace: string, + fieldPath: FieldPath, + newName: string + ) => void; onDiagramBackgroundClicked: () => void; onDeleteCollection: (ns: string) => void; onDeleteRelationship: (rId: string) => void; @@ -170,6 +176,7 @@ const DiagramContent: React.FunctionComponent<{ onCollectionSelect, onRelationshipSelect, onFieldSelect, + onRenameField, onDiagramBackgroundClicked, onCreateNewRelationship, onRelationshipDrawn, @@ -385,6 +392,7 @@ const DiagramContent: React.FunctionComponent<{ onPaneClick, onEdgeClick, onFieldClick, + onFieldNameChange: onRenameField, onNodeDragStop, onConnect, } satisfies DiagramProps), @@ -399,6 +407,7 @@ const DiagramContent: React.FunctionComponent<{ onPaneClick, onEdgeClick, onFieldClick, + onRenameField, onNodeDragStop, onConnect, ] @@ -461,6 +470,7 @@ const ConnectedDiagramContent = connect( onCollectionSelect: selectCollection, onRelationshipSelect: selectRelationship, onFieldSelect: selectField, + onRenameField: renameField, onDiagramBackgroundClicked: selectBackground, onCreateNewRelationship: createNewRelationship, onDeleteCollection: deleteCollection, diff --git a/packages/compass-data-modeling/src/utils/nodes-and-edges.spec.ts b/packages/compass-data-modeling/src/utils/nodes-and-edges.spec.ts index e7a7c4aa941..a542cad477d 100644 --- a/packages/compass-data-modeling/src/utils/nodes-and-edges.spec.ts +++ b/packages/compass-data-modeling/src/utils/nodes-and-edges.spec.ts @@ -13,17 +13,29 @@ describe('getFieldsFromSchema', function () { expect(result).to.deep.equal([]); }); - it('returns fields for a simple schema', function () { + it('returns fields for a simple schema, with non editable _id', function () { const result = getFieldsFromSchema({ jsonSchema: { bsonType: 'object', properties: { + _id: { bsonType: 'objectId' }, name: { bsonType: 'string' }, age: { bsonType: 'int' }, }, }, }); expect(result).to.deep.equal([ + { + name: '_id', + id: ['_id'], + type: 'objectId', + depth: 0, + glyphs: ['key'], + selectable: true, + selected: false, + variant: undefined, + editable: false, + }, { name: 'name', id: ['name'], @@ -33,6 +45,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'age', @@ -43,6 +56,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, ]); }); @@ -65,6 +79,7 @@ describe('getFieldsFromSchema', function () { selected: false, type: ['int', 'string'], variant: undefined, + editable: true, }); }); @@ -90,6 +105,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'age', @@ -100,6 +116,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: 'preview', + editable: true, }, { name: 'profession', @@ -110,6 +127,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, ]); }); @@ -136,6 +154,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'age', @@ -146,6 +165,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: 'preview', + editable: true, }, { name: 'profession', @@ -156,6 +176,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: 'preview', + editable: true, }, ]); }); @@ -193,6 +214,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'name', @@ -203,6 +225,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'address', @@ -213,6 +236,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'street', @@ -223,6 +247,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'city', @@ -233,6 +258,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, ]); }); @@ -269,6 +295,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'name', @@ -279,6 +306,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'address', @@ -289,6 +317,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'street', @@ -299,6 +328,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: 'preview', + editable: true, }, { name: 'city', @@ -309,6 +339,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, ]); }); @@ -355,6 +386,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'name', @@ -365,6 +397,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'address', @@ -375,6 +408,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'street', @@ -385,6 +419,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: 'preview', + editable: true, }, { name: 'city', @@ -395,6 +430,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'billingAddress', @@ -405,6 +441,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'street', @@ -415,6 +452,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'city', @@ -425,6 +463,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: 'preview', + editable: true, }, ]); }); @@ -457,6 +496,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'title', @@ -467,6 +507,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'completed', @@ -477,6 +518,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, ]); }); @@ -511,6 +553,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }); expect(result[1]).to.deep.equal({ name: 'first', @@ -521,6 +564,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }); expect(result[2]).to.deep.equal({ name: 'last', @@ -531,6 +575,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }); }); @@ -567,6 +612,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'title', @@ -577,6 +623,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, { name: 'completed', @@ -587,6 +634,7 @@ describe('getFieldsFromSchema', function () { selectable: true, selected: false, variant: undefined, + editable: true, }, ]); }); diff --git a/packages/compass-data-modeling/src/utils/nodes-and-edges.ts b/packages/compass-data-modeling/src/utils/nodes-and-edges.ts index 14a31584a08..a3fc22d0ec7 100644 --- a/packages/compass-data-modeling/src/utils/nodes-and-edges.ts +++ b/packages/compass-data-modeling/src/utils/nodes-and-edges.ts @@ -13,7 +13,7 @@ import type { Relationship, } from '../services/data-model-storage'; import { traverseSchema } from './schema-traversal'; -import { areFieldPathsEqual } from './utils'; +import { areFieldPathsEqual, isIdField } from './utils'; const NO_HIGHLIGHTED_FIELDS = {}; @@ -100,6 +100,7 @@ export const getFieldsFromSchema = ({ selected: !!selectedField?.length && areFieldPathsEqual(fieldPath, selectedField), + editable: !isIdField(fieldPath), variant: highlightedFields.length && highlightedFields.some((highlightedField) => From 7fffb59852f872b1ac629f68a5047b5aff660c17 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Thu, 16 Oct 2025 17:53:43 +0200 Subject: [PATCH 2/7] add e2e --- .../compass-e2e-tests/helpers/selectors.ts | 5 + .../tests/data-modeling-tab.test.ts | 568 ++++++++++-------- 2 files changed, 330 insertions(+), 243 deletions(-) diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index 6eddaac9c92..4d0e4930d3d 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -1476,6 +1476,7 @@ export const DataModelsListItemDeleteButton = `[data-action="delete"]`; export const DataModelAddRelationshipBtn = 'aria/Add Relationship'; export const DataModelAddCollectionBtn = 'aria/Add Collection'; export const DataModelNameInputLabel = '//label[text()="Name"]'; +export const DataModelFieldNameInputLabel = '//label[text()="Field name"]'; export const DataModelNameInput = 'input[data-testid="data-model-collection-drawer-name-input"]'; export const DataModelRelationshipLocalCollectionSelect = @@ -1499,6 +1500,10 @@ export const DataModelCollectionRelationshipItemDelete = `[aria-label="Delete re export const DataModelCollectionSidebarItemDelete = `[aria-label="Delete collection"]`; export const DataModelCollectionSidebarItemDeleteButton = `[data-action="delete"]`; export const DataModelInfoBannerCloseBtn = `[data-testid="data-info-banner"] [aria-label="Close Message"]`; +export const DataModelAddFieldBtn = '[aria-label="Add Field"]'; +export const DataModelDiagramField = (fieldName: string) => + `//*[text()="${fieldName}"]`; +export const DataModelDiagramFieldInput = 'input[title="Edit field name"]'; // Side drawer export const SideDrawer = `[data-testid="${getDrawerIds().root}"]`; 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 346fbbdf077..a4d91248566 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -122,12 +122,7 @@ async function setupDiagram( await browser.clickVisible(Selectors.DataModelInfoBannerCloseBtn); } -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 +async function closeDrawerIfOpen(browser: CompassBrowser) { const drawer = browser.$(Selectors.SideDrawer); if ( (await drawer.isDisplayed()) && @@ -136,6 +131,15 @@ async function selectCollectionOnTheDiagram( await browser.clickVisible(Selectors.SideDrawerCloseButton); await drawer.waitForDisplayed({ reverse: true }); } +} + +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 + await closeDrawerIfOpen(browser); // Click on the collection node to open the drawer const collectionNode = browser.$(Selectors.DataModelPreviewCollection(ns)); @@ -148,7 +152,7 @@ async function selectCollectionOnTheDiagram( y: 15, }); - await drawer.waitForDisplayed(); + await browser.$(Selectors.SideDrawer).waitForDisplayed(); const collectionName = await browser.getInputByLabel( browser.$(Selectors.SideDrawer).$(Selectors.DataModelNameInputLabel) @@ -305,283 +309,303 @@ describe('Data Modeling tab', function () { expect(nodes).to.have.lengthOf(2); expect(nodes[0].id).to.equal('test.testCollection-one'); expect(nodes[1].id).to.equal('test.testCollection-two'); + }); - // Apply change to the model + context('Undo/Redo and Storage', function () { + it('actions are undoable and persist after re-opening', async function () { + const dataModelName = 'Test Data Model'; + await setupDiagram(browser, { + diagramName: dataModelName, + connectionName: DEFAULT_CONNECTION_NAME_1, + databaseName: 'test', + }); - // react flow uses its own coordinate system, - // so we get the node element location for the pointer action - const testCollection1 = browser.$( - Selectors.DataModelPreviewCollection('test.testCollection-one') - ); - const startPosition = await dragNode( - browser, - Selectors.DataModelPreviewCollection('test.testCollection-one'), - { x: 100, y: 0 } - ); - await browser.waitForAnimations(dataModelEditor); - - // Check that the first node has moved and mark the new position - const newPosition = await testCollection1.getLocation(); - expect(newPosition).not.to.deep.equal(startPosition); - - // Undo the change - await browser.clickVisible(Selectors.DataModelUndoButton); - await browser.waitForAnimations(dataModelEditor); - const positionAfterUndone = await testCollection1.getLocation(); - expect(positionAfterUndone).to.deep.equal(startPosition); - - // Redo the change - await browser.waitForAriaDisabled(Selectors.DataModelRedoButton, false); - await browser.clickVisible(Selectors.DataModelRedoButton); - await browser.waitForAnimations(dataModelEditor); - const positionAfterRedo = await testCollection1.getLocation(); - expect(positionAfterRedo).to.deep.equal(newPosition); - // Open a new tab - await browser.openNewTab(); - - // Open the saved diagram - await browser.clickVisible(Selectors.DataModelsListItem(dataModelName)); - await browser.$(Selectors.DataModelEditor).waitForDisplayed(); - - // TODO: Verify that the diagram has the latest changes COMPASS-9479 - const savedNodes = await getDiagramNodes(browser, 2); - expect(savedNodes).to.have.lengthOf(2); - - // Open a new tab - await browser.openNewTab(); - - // Delete the saved diagram - await browser.clickVisible( - Selectors.DataModelsListItemActions(dataModelName) - ); - await browser.clickVisible(Selectors.DataModelsListItemDeleteButton); - await browser.clickVisible(Selectors.confirmationModalConfirmButton()); - await browser - .$(Selectors.DataModelsListItem(dataModelName)) - .waitForDisplayed({ reverse: true }); - }); + const dataModelEditor = browser.$(Selectors.DataModelEditor); + await dataModelEditor.waitForDisplayed(); - it('allows undo after opening a diagram', async function () { - const dataModelName = 'Test Data Model - Undo After Open'; - await setupDiagram(browser, { - diagramName: dataModelName, - connectionName: DEFAULT_CONNECTION_NAME_1, - databaseName: 'test', + const nodes = await getDiagramNodes(browser, 2); + expect(nodes).to.have.lengthOf(2); + expect(nodes[0].id).to.equal('test.testCollection-one'); + expect(nodes[1].id).to.equal('test.testCollection-two'); + + // Apply change to the model + + // react flow uses its own coordinate system, + // so we get the node element location for the pointer action + const testCollection1 = browser.$( + Selectors.DataModelPreviewCollection('test.testCollection-one') + ); + const startPosition = await dragNode( + browser, + Selectors.DataModelPreviewCollection('test.testCollection-one'), + { x: 100, y: 0 } + ); + await browser.waitForAnimations(dataModelEditor); + + // Check that the first node has moved and mark the new position + const newPosition = await testCollection1.getLocation(); + expect(newPosition).not.to.deep.equal(startPosition); + + // Undo the change + await browser.clickVisible(Selectors.DataModelUndoButton); + await browser.waitForAnimations(dataModelEditor); + const positionAfterUndone = await testCollection1.getLocation(); + expect(positionAfterUndone).to.deep.equal(startPosition); + + // Redo the change + await browser.waitForAriaDisabled(Selectors.DataModelRedoButton, false); + await browser.clickVisible(Selectors.DataModelRedoButton); + await browser.waitForAnimations(dataModelEditor); + const positionAfterRedo = await testCollection1.getLocation(); + expect(positionAfterRedo).to.deep.equal(newPosition); + // Open a new tab + await browser.openNewTab(); + + // Open the saved diagram + await browser.clickVisible(Selectors.DataModelsListItem(dataModelName)); + await browser.$(Selectors.DataModelEditor).waitForDisplayed(); + + const savedNodes = await getDiagramNodes(browser, 2); + expect(savedNodes).to.have.lengthOf(2); + + // Open a new tab + await browser.openNewTab(); + + // Delete the saved diagram + await browser.clickVisible( + Selectors.DataModelsListItemActions(dataModelName) + ); + await browser.clickVisible(Selectors.DataModelsListItemDeleteButton); + await browser.clickVisible(Selectors.confirmationModalConfirmButton()); + await browser + .$(Selectors.DataModelsListItem(dataModelName)) + .waitForDisplayed({ reverse: true }); }); - const dataModelEditor = browser.$(Selectors.DataModelEditor); - await dataModelEditor.waitForDisplayed(); + it('allows undo after opening a diagram', async function () { + const dataModelName = 'Test Data Model - Undo After Open'; + await setupDiagram(browser, { + diagramName: dataModelName, + connectionName: DEFAULT_CONNECTION_NAME_1, + databaseName: 'test', + }); - await dragNode( - browser, - Selectors.DataModelPreviewCollection('test.testCollection-one'), - { x: 100, y: 0 } - ); - await browser.waitForAnimations(dataModelEditor); + const dataModelEditor = browser.$(Selectors.DataModelEditor); + await dataModelEditor.waitForDisplayed(); - // Open the saved diagram in new tab - await browser.openNewTab(); - await browser.clickVisible(Selectors.DataModelsListItem(dataModelName)); - await browser.$(Selectors.DataModelEditor).waitForDisplayed(); + await dragNode( + browser, + Selectors.DataModelPreviewCollection('test.testCollection-one'), + { x: 100, y: 0 } + ); + await browser.waitForAnimations(dataModelEditor); - // Ensure that undo button is enabled - await browser.waitForAriaDisabled(Selectors.DataModelUndoButton, false); + // Open the saved diagram in new tab + await browser.openNewTab(); + await browser.clickVisible(Selectors.DataModelsListItem(dataModelName)); + await browser.$(Selectors.DataModelEditor).waitForDisplayed(); - // Undo the change - await browser.clickVisible(Selectors.DataModelUndoButton); - await browser.waitForAnimations(dataModelEditor); + // Ensure that undo button is enabled + await browser.waitForAriaDisabled(Selectors.DataModelUndoButton, false); - // Ensure that undo button is now disabled and redo is enabled - await browser.waitForAriaDisabled(Selectors.DataModelUndoButton, true); - await browser.waitForAriaDisabled(Selectors.DataModelRedoButton, false); - }); + // Undo the change + await browser.clickVisible(Selectors.DataModelUndoButton); + await browser.waitForAnimations(dataModelEditor); - it('exports the data model to JSON', async function () { - const dataModelName = 'Test Export Model - JSON'; - exportFileName = `${dataModelName}.json`; - await setupDiagram(browser, { - diagramName: dataModelName, - connectionName: DEFAULT_CONNECTION_NAME_1, - databaseName: 'test', + // Ensure that undo button is now disabled and redo is enabled + await browser.waitForAriaDisabled(Selectors.DataModelUndoButton, true); + await browser.waitForAriaDisabled(Selectors.DataModelRedoButton, false); }); + }); + + context('Export', function () { + it('exports the data model to JSON', async function () { + const dataModelName = 'Test Export Model - JSON'; + exportFileName = `${dataModelName}.json`; + await setupDiagram(browser, { + diagramName: dataModelName, + connectionName: DEFAULT_CONNECTION_NAME_1, + databaseName: 'test', + }); - await browser.clickVisible(Selectors.DataModelExportButton); - const exportModal = browser.$(Selectors.DataModelExportModal); - await exportModal.waitForDisplayed(); + await browser.clickVisible(Selectors.DataModelExportButton); + const exportModal = browser.$(Selectors.DataModelExportModal); + await exportModal.waitForDisplayed(); - await browser.clickParent(Selectors.DataModelExportJsonOption); - await browser.clickVisible(Selectors.DataModelExportModalConfirmButton); + await browser.clickParent(Selectors.DataModelExportJsonOption); + await browser.clickVisible(Selectors.DataModelExportModalConfirmButton); - const { fileExists, filePath } = await waitForFileDownload( - exportFileName, - browser - ); - expect(fileExists).to.be.true; - - const content = readFileSync(filePath, 'utf-8'); - const model = JSON.parse(content); - - // Within beforeEach hook, we create these two collections - expect(model).to.deep.equal({ - collections: { - 'test.testCollection-one': { - ns: 'test.testCollection-one', - jsonSchema: { - bsonType: 'object', - required: ['_id', 'i', 'iString', 'j'], - properties: { - _id: { - bsonType: 'objectId', - }, - i: { - bsonType: 'int', - }, - iString: { - bsonType: 'string', - }, - j: { - bsonType: 'int', + const { fileExists, filePath } = await waitForFileDownload( + exportFileName, + browser + ); + expect(fileExists).to.be.true; + + const content = readFileSync(filePath, 'utf-8'); + const model = JSON.parse(content); + + // Within beforeEach hook, we create these two collections + expect(model).to.deep.equal({ + collections: { + 'test.testCollection-one': { + ns: 'test.testCollection-one', + jsonSchema: { + bsonType: 'object', + required: ['_id', 'i', 'iString', 'j'], + properties: { + _id: { + bsonType: 'objectId', + }, + i: { + bsonType: 'int', + }, + iString: { + bsonType: 'string', + }, + j: { + bsonType: 'int', + }, }, }, }, - }, - 'test.testCollection-two': { - ns: 'test.testCollection-two', - jsonSchema: { - bsonType: 'object', - required: ['_id', 'i', 'j'], - properties: { - _id: { - bsonType: 'objectId', - }, - i: { - bsonType: 'int', - }, - j: { - bsonType: 'int', + 'test.testCollection-two': { + ns: 'test.testCollection-two', + jsonSchema: { + bsonType: 'object', + required: ['_id', 'i', 'j'], + properties: { + _id: { + bsonType: 'objectId', + }, + i: { + bsonType: 'int', + }, + j: { + bsonType: 'int', + }, }, }, }, }, - }, - relationships: [], - }); - }); - - it('exports the data model to PNG', async function () { - if (process.platform === 'win32') { - console.warn('Skipping PNG export test on Windows'); - this.skip(); - } - const dataModelName = 'Test Export Model - PNG'; - exportFileName = `${dataModelName}.png`; - await setupDiagram(browser, { - diagramName: dataModelName, - connectionName: DEFAULT_CONNECTION_NAME_1, - databaseName: 'test', + relationships: [], + }); }); - await browser.clickVisible(Selectors.DataModelExportButton); - const exportModal = browser.$(Selectors.DataModelExportModal); - await exportModal.waitForDisplayed(); + it('exports the data model to PNG', async function () { + if (process.platform === 'win32') { + console.warn('Skipping PNG export test on Windows'); + this.skip(); + } + const dataModelName = 'Test Export Model - PNG'; + exportFileName = `${dataModelName}.png`; + await setupDiagram(browser, { + diagramName: dataModelName, + connectionName: DEFAULT_CONNECTION_NAME_1, + databaseName: 'test', + }); - await browser.clickParent(Selectors.DataModelExportPngOption); - await browser.clickVisible(Selectors.DataModelExportModalConfirmButton); + await browser.clickVisible(Selectors.DataModelExportButton); + const exportModal = browser.$(Selectors.DataModelExportModal); + await exportModal.waitForDisplayed(); - const { fileExists, filePath } = await waitForFileDownload( - exportFileName, - browser - ); - expect(fileExists).to.be.true; + await browser.clickParent(Selectors.DataModelExportPngOption); + await browser.clickVisible(Selectors.DataModelExportModalConfirmButton); - const { data } = await recognize(filePath, 'eng', { - cachePath: tmpdir, - }); + const { fileExists, filePath } = await waitForFileDownload( + exportFileName, + browser + ); + expect(fileExists).to.be.true; - const text = data.text.toLowerCase(); + const { data } = await recognize(filePath, 'eng', { + cachePath: tmpdir, + }); - expect(text).to.include('testCollection-one'.toLowerCase()); - expect(text).to.include('testCollection-two'.toLowerCase()); + const text = data.text.toLowerCase(); - expect(text).to.include('id objectId'.toLowerCase()); - expect(text).to.include('i int'); - // Disabled as it's not recognized correctly by tesseract.js at the moment. - // expect(text).to.include('j int'); - // it does not correctly recognize `iString` and only returns `String`. - // its already good enough to verify this for now and if it flakes - // more, we may need to revisit this test. - expect(text).to.include('String string'.toLowerCase()); - }); + expect(text).to.include('testCollection-one'.toLowerCase()); + expect(text).to.include('testCollection-two'.toLowerCase()); - it('exports the data model to compass format and imports it back', async function () { - const dataModelName = 'Test Export Model - Save-Open'; - exportFileName = `${dataModelName}.mdm`; - await setupDiagram(browser, { - diagramName: dataModelName, - connectionName: DEFAULT_CONNECTION_NAME_1, - databaseName: 'test', + expect(text).to.include('id objectId'.toLowerCase()); + expect(text).to.include('i int'); + // Disabled as it's not recognized correctly by tesseract.js at the moment. + // expect(text).to.include('j int'); + // it does not correctly recognize `iString` and only returns `String`. + // its already good enough to verify this for now and if it flakes + // more, we may need to revisit this test. + expect(text).to.include('String string'.toLowerCase()); }); - const dataModelEditor = browser.$(Selectors.DataModelEditor); - await dataModelEditor.waitForDisplayed(); + it('exports the data model to compass format and imports it back', async function () { + const dataModelName = 'Test Export Model - Save-Open'; + exportFileName = `${dataModelName}.mdm`; + await setupDiagram(browser, { + diagramName: dataModelName, + connectionName: DEFAULT_CONNECTION_NAME_1, + databaseName: 'test', + }); - await dragNode( - browser, - Selectors.DataModelPreviewCollection('test.testCollection-one'), - { x: 100, y: 0 } - ); + const dataModelEditor = browser.$(Selectors.DataModelEditor); + await dataModelEditor.waitForDisplayed(); + + await dragNode( + browser, + Selectors.DataModelPreviewCollection('test.testCollection-one'), + { x: 100, y: 0 } + ); - await browser.waitForAnimations(dataModelEditor); + await browser.waitForAnimations(dataModelEditor); - await browser.clickVisible(Selectors.DataModelExportButton); - const exportModal = browser.$(Selectors.DataModelExportModal); - await exportModal.waitForDisplayed(); + await browser.clickVisible(Selectors.DataModelExportButton); + const exportModal = browser.$(Selectors.DataModelExportModal); + await exportModal.waitForDisplayed(); - await browser.clickParent(Selectors.DataModelExportDiagramOption); - await browser.clickVisible(Selectors.DataModelExportModalConfirmButton); + await browser.clickParent(Selectors.DataModelExportDiagramOption); + await browser.clickVisible(Selectors.DataModelExportModalConfirmButton); - const { fileExists, filePath } = await waitForFileDownload( - exportFileName, - browser - ); - expect(fileExists).to.be.true; + const { fileExists, filePath } = await waitForFileDownload( + exportFileName, + browser + ); + expect(fileExists).to.be.true; - const content = readFileSync(filePath, 'utf-8'); - const model = JSON.parse(content); + const content = readFileSync(filePath, 'utf-8'); + const model = JSON.parse(content); - expect(model.name).to.equal(dataModelName); + expect(model.name).to.equal(dataModelName); - const edits = JSON.parse( - Buffer.from(model.edits, 'base64').toString('utf-8') - ); - expect(edits).to.be.an('array').of.length(2); - expect(edits[0].type).to.equal('SetModel'); - expect(edits[1].type).to.equal('MoveCollection'); - - // Open the saved diagram - await browser.closeWorkspaceTabs(); - await browser.navigateToDataModeling(); - - await browser.selectFile(Selectors.ImportDataModelInput, filePath); - await browser.$(Selectors.DataModelEditor).waitForDisplayed(); - const savedNodes = await getDiagramNodes(browser, 2); - - expect(savedNodes).to.have.lengthOf(2); - expect(savedNodes[0].id).to.equal('test.testCollection-one'); - expect(savedNodes[1].id).to.equal('test.testCollection-two'); - - // Ensure that two diagrams exist (with correct incremental name) - await browser.closeWorkspaceTabs(); - await browser.navigateToDataModeling(); - - const cardsSelector = Selectors.DataModelsListItem(); - await browser.waitForAnimations(cardsSelector); - const titles = await browser - .$$(cardsSelector) - .map((element) => element.getAttribute('data-diagram-name')); - expect(titles).to.include(dataModelName); - // The second one is the one we just opened - expect(titles).to.include(`${dataModelName} (1)`); + const edits = JSON.parse( + Buffer.from(model.edits, 'base64').toString('utf-8') + ); + expect(edits).to.be.an('array').of.length(2); + expect(edits[0].type).to.equal('SetModel'); + expect(edits[1].type).to.equal('MoveCollection'); + + // Open the saved diagram + await browser.closeWorkspaceTabs(); + await browser.navigateToDataModeling(); + + await browser.selectFile(Selectors.ImportDataModelInput, filePath); + await browser.$(Selectors.DataModelEditor).waitForDisplayed(); + const savedNodes = await getDiagramNodes(browser, 2); + + expect(savedNodes).to.have.lengthOf(2); + expect(savedNodes[0].id).to.equal('test.testCollection-one'); + expect(savedNodes[1].id).to.equal('test.testCollection-two'); + + // Ensure that two diagrams exist (with correct incremental name) + await browser.closeWorkspaceTabs(); + await browser.navigateToDataModeling(); + + const cardsSelector = Selectors.DataModelsListItem(); + await browser.waitForAnimations(cardsSelector); + const titles = await browser + .$$(cardsSelector) + .map((element) => element.getAttribute('data-diagram-name')); + expect(titles).to.include(dataModelName); + // The second one is the one we just opened + expect(titles).to.include(`${dataModelName} (1)`); + }); }); context('Drawer and Diagram interactions', function () { @@ -849,5 +873,63 @@ describe('Data Modeling tab', function () { await browser.keys([Key.Command, 'z']); await getDiagramNodes(browser, 2); }); + + it('selecting, adding & editing fields via the diagram', async function () { + const dataModelName = 'Test Data Model'; + await setupDiagram(browser, { + diagramName: dataModelName, + connectionName: DEFAULT_CONNECTION_NAME_1, + databaseName: 'test', + }); + + const dataModelEditor = browser.$(Selectors.DataModelEditor); + await dataModelEditor.waitForDisplayed(); + + // Ensure that we see the collection + const testCollection1 = browser.$( + Selectors.DataModelPreviewCollection('test.testCollection-one') + ); + await testCollection1.waitForDisplayed(); + + // Selecting an existing field should open it in the drawer + const existingField = testCollection1.$( + Selectors.DataModelDiagramField('iString') + ); + await existingField.waitForDisplayed(); + await existingField.click(); + const drawerEF = browser.$(Selectors.SideDrawer); + await drawerEF.waitForDisplayed(); + const existingFieldNameInTheDrawer = await browser.getInputByLabel( + drawerEF.$(Selectors.DataModelFieldNameInputLabel) + ); + expect(await existingFieldNameInTheDrawer.getValue()).to.equal('iString'); + + // Click on the + button of the collection to add a new field + await closeDrawerIfOpen(browser); + const addFieldButton = testCollection1.$(Selectors.DataModelAddFieldBtn); + await addFieldButton.waitForClickable(); + await addFieldButton.click(); + + // Find the new field - 'field-1' and edit its name + await closeDrawerIfOpen(browser); + const newField = testCollection1.$( + Selectors.DataModelDiagramField('field-1') + ); + await newField.waitForDisplayed(); + await newField.doubleClick(); + + const fieldNameInput = browser.$(Selectors.DataModelDiagramFieldInput); + await fieldNameInput.waitForDisplayed(); + await browser.keys('Gandalf'); + await browser.keys('Enter'); + + // Verify that the name is updated in the drawer + const drawerNF = browser.$(Selectors.SideDrawer); + await drawerNF.waitForDisplayed(); + const newFieldNameInDrawer = await browser.getInputByLabel( + drawerNF.$(Selectors.DataModelFieldNameInputLabel) + ); + expect(await newFieldNameInDrawer.getValue()).to.equal('Gandalf'); + }); }); }); From 3ba08f854cab5b71b1c26445e4750844224523a9 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 17 Oct 2025 10:00:07 +0200 Subject: [PATCH 3/7] update name --- packages/compass-e2e-tests/tests/data-modeling-tab.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a4d91248566..36a02aed38b 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -313,7 +313,7 @@ describe('Data Modeling tab', function () { context('Undo/Redo and Storage', function () { it('actions are undoable and persist after re-opening', async function () { - const dataModelName = 'Test Data Model'; + const dataModelName = 'Test Data Model - Undo/Redo'; await setupDiagram(browser, { diagramName: dataModelName, connectionName: DEFAULT_CONNECTION_NAME_1, From b3ab579442b4198758e9a77ea6b53a220c3e6311 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 17 Oct 2025 10:22:25 +0200 Subject: [PATCH 4/7] improve field test --- packages/compass-e2e-tests/tests/data-modeling-tab.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 36a02aed38b..bb0cae0d620 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -875,7 +875,7 @@ describe('Data Modeling tab', function () { }); it('selecting, adding & editing fields via the diagram', async function () { - const dataModelName = 'Test Data Model'; + const dataModelName = 'Test Data Model - Fields via Diagram'; await setupDiagram(browser, { diagramName: dataModelName, connectionName: DEFAULT_CONNECTION_NAME_1, @@ -920,7 +920,8 @@ describe('Data Modeling tab', function () { const fieldNameInput = browser.$(Selectors.DataModelDiagramFieldInput); await fieldNameInput.waitForDisplayed(); - await browser.keys('Gandalf'); + await fieldNameInput.addValue('Gandalf'); + await browser.pause(1000); // wait for the value to be updated await browser.keys('Enter'); // Verify that the name is updated in the drawer From 4388560af9cf5a27a17222cbdee3cce996a96462 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 22 Oct 2025 15:08:22 +0200 Subject: [PATCH 5/7] try different doubleclick --- .../tests/data-modeling-tab.test.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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 bb0cae0d620..7722d6046c5 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -43,6 +43,20 @@ type DiagramInstance = { getEdges: () => Array; }; +async function doubleClickElement( + browser: CompassBrowser, + element: ChainablePromiseElement +) { + await browser + .action('pointer') + .move({ origin: element }) + .down({ button: 0 }) + .up({ button: 0 }) + .down({ button: 0 }) + .up({ button: 0 }) + .perform(); +} + /** * Clicks on a specific element at the given coordinates. * element.click({ x: number, y: number }) doesn't work as expected, @@ -912,11 +926,16 @@ describe('Data Modeling tab', function () { // Find the new field - 'field-1' and edit its name await closeDrawerIfOpen(browser); + console.log( + 'DEBUG', + Selectors.DataModelDiagramField('field-1'), + Selectors.DataModelDiagramFieldInput + ); const newField = testCollection1.$( Selectors.DataModelDiagramField('field-1') ); await newField.waitForDisplayed(); - await newField.doubleClick(); + await doubleClickElement(browser, newField); const fieldNameInput = browser.$(Selectors.DataModelDiagramFieldInput); await fieldNameInput.waitForDisplayed(); From 355934b3f368864edb58b17653441136f4e67aa8 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 22 Oct 2025 17:42:54 +0200 Subject: [PATCH 6/7] update e2e --- .../tests/data-modeling-tab.test.ts | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) 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 7722d6046c5..55de23ba695 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -888,7 +888,7 @@ describe('Data Modeling tab', function () { await getDiagramNodes(browser, 2); }); - it('selecting, adding & editing fields via the diagram', async function () { + it('selecting and adding fields via the diagram, editing via the sidebar', async function () { const dataModelName = 'Test Data Model - Fields via Diagram'; await setupDiagram(browser, { diagramName: dataModelName, @@ -905,19 +905,6 @@ describe('Data Modeling tab', function () { ); await testCollection1.waitForDisplayed(); - // Selecting an existing field should open it in the drawer - const existingField = testCollection1.$( - Selectors.DataModelDiagramField('iString') - ); - await existingField.waitForDisplayed(); - await existingField.click(); - const drawerEF = browser.$(Selectors.SideDrawer); - await drawerEF.waitForDisplayed(); - const existingFieldNameInTheDrawer = await browser.getInputByLabel( - drawerEF.$(Selectors.DataModelFieldNameInputLabel) - ); - expect(await existingFieldNameInTheDrawer.getValue()).to.equal('iString'); - // Click on the + button of the collection to add a new field await closeDrawerIfOpen(browser); const addFieldButton = testCollection1.$(Selectors.DataModelAddFieldBtn); @@ -925,31 +912,27 @@ describe('Data Modeling tab', function () { await addFieldButton.click(); // Find the new field - 'field-1' and edit its name + // (via the drawer - inline edit is too flaky) await closeDrawerIfOpen(browser); - console.log( - 'DEBUG', - Selectors.DataModelDiagramField('field-1'), - Selectors.DataModelDiagramFieldInput - ); const newField = testCollection1.$( Selectors.DataModelDiagramField('field-1') ); await newField.waitForDisplayed(); - await doubleClickElement(browser, newField); + await newField.click(); - const fieldNameInput = browser.$(Selectors.DataModelDiagramFieldInput); - await fieldNameInput.waitForDisplayed(); - await fieldNameInput.addValue('Gandalf'); - await browser.pause(1000); // wait for the value to be updated - await browser.keys('Enter'); - - // Verify that the name is updated in the drawer const drawerNF = browser.$(Selectors.SideDrawer); await drawerNF.waitForDisplayed(); - const newFieldNameInDrawer = await browser.getInputByLabel( + const newFieldNameInTheDrawer = await browser.getInputByLabel( drawerNF.$(Selectors.DataModelFieldNameInputLabel) ); - expect(await newFieldNameInDrawer.getValue()).to.equal('Gandalf'); + await newFieldNameInTheDrawer.setValue('Gandalf'); + await drawerNF.click(); // Unfocus the input. + + // Verify that the name is updated in the diagram + const renamedField = testCollection1.$( + Selectors.DataModelDiagramField('Gandalf') + ); + await renamedField.waitForDisplayed(); }); }); }); From eaf8089f1b3535a2981080db55f78b6d7f84308c Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 22 Oct 2025 18:55:57 +0200 Subject: [PATCH 7/7] cleanup --- .../tests/data-modeling-tab.test.ts | 14 -------------- 1 file changed, 14 deletions(-) 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 55de23ba695..67dbe7e5f54 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -43,20 +43,6 @@ type DiagramInstance = { getEdges: () => Array; }; -async function doubleClickElement( - browser: CompassBrowser, - element: ChainablePromiseElement -) { - await browser - .action('pointer') - .move({ origin: element }) - .down({ button: 0 }) - .up({ button: 0 }) - .down({ button: 0 }) - .up({ button: 0 }) - .perform(); -} - /** * Clicks on a specific element at the given coordinates. * element.click({ x: number, y: number }) doesn't work as expected,