Skip to content
257 changes: 239 additions & 18 deletions packages/compass-collection/src/stores/collection-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
import { type CollectionMetadata } from 'mongodb-collection-model';
import type { types } from '@mongodb-js/mdb-experiment-js';

// Wait time in ms for async operations to complete
const WAIT_TIME = 50;

// Helper function to create proper mock assignment objects for testing
const createMockAssignment = (
variant: ExperimentTestGroup
Expand Down Expand Up @@ -86,12 +89,36 @@ describe('Collection Tab Content store', function () {
const sandbox = Sinon.createSandbox();

const localAppRegistry = sandbox.spy(new AppRegistry());
const globalAppRegistry = sandbox.spy(new AppRegistry());
const analyzeCollectionSchemaStub = sandbox
.stub(collectionTabModule, 'analyzeCollectionSchema')
.returns(async () => {});

const dataService = {} as any;
const atlasAiService = {} as any;
// Mock the cleanup helper to track event listeners
const eventListeners = new Map<string, ((...args: unknown[]) => void)[]>();
const mockCleanupHelper = {
on: sandbox.spy(
(
_registry: unknown,
eventName: string,
listener: (...args: unknown[]) => void
) => {
if (!eventListeners.has(eventName)) {
eventListeners.set(eventName, []);
}
eventListeners.get(eventName)!.push(listener);
}
),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use new EventEmitter() instead of building a custom tracker here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated tests!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any updates in that direction but this was a suggestion, not a strict requirement

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m a little unsure what you’re suggesting, could you clarify?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, looking into this in more detail ... why are we providing these mocks here? Tests seem to pass fine (and they should) if we just use the "real" helpers directly, and we're not spying on the listeners here or anything like that:

diff --git a/packages/compass-collection/src/stores/collection-tab.spec.ts b/packages/compass-collection/src/stores/collection-tab.spec.ts
index 1d11548755ff..b775583f19aa 100644
--- a/packages/compass-collection/src/stores/collection-tab.spec.ts
+++ b/packages/compass-collection/src/stores/collection-tab.spec.ts
@@ -4,7 +4,8 @@ import { selectTab } from '../modules/collection-tab';
 import * as collectionTabModule from '../modules/collection-tab';
 import { waitFor } from '@mongodb-js/testing-library-compass';
 import Sinon from 'sinon';
-import AppRegistry from '@mongodb-js/compass-app-registry';
+import type { ActivateHelpers } from '@mongodb-js/compass-app-registry';
+import AppRegistry, { createActivateHelpers } from '@mongodb-js/compass-app-registry';
 import { expect } from 'chai';
 import type { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider';
 import type { ExperimentationServices } from '@mongodb-js/compass-telemetry/provider';
@@ -94,33 +95,7 @@ describe('Collection Tab Content store', function () {
     .stub(collectionTabModule, 'analyzeCollectionSchema')
     .returns(async () => {});
 
-  // Track event listeners for proper cleanup
-  const eventListeners: Array<{
-    registry: AppRegistry;
-    eventName: string;
-    listener: (...args: unknown[]) => void;
-  }> = [];
-
-  const mockActivateHelpers = {
-    on: sandbox.spy(
-      (
-        registry: AppRegistry,
-        eventName: string,
-        listener: (...args: unknown[]) => void
-      ) => {
-        registry.on(eventName, listener);
-        eventListeners.push({ registry, eventName, listener });
-      }
-    ),
-    cleanup: sandbox.spy(() => {
-      // Remove all tracked event listeners
-      eventListeners.forEach(({ registry, eventName, listener }) => {
-        registry.removeListener(eventName, listener);
-      });
-      eventListeners.length = 0;
-    }),
-    addCleanup: sandbox.spy(),
-  };
+  let mockActivateHelpers: ActivateHelpers;
 
   const dataService = {} as never;
   const atlasAiService = {} as never;
@@ -168,7 +143,7 @@ describe('Collection Tab Content store', function () {
         logger,
         preferences,
       },
-      mockActivateHelpers as never
+      mockActivateHelpers
     ));
     await waitFor(() => {
       expect(store.getState())
@@ -178,6 +153,10 @@ describe('Collection Tab Content store', function () {
     return store;
   };
 
+  beforeEach(function() {
+    mockActivateHelpers = createActivateHelpers();
+  });
+
   afterEach(function () {
     mockActivateHelpers.cleanup();
     sandbox.resetHistory();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(also, I'd strongly recommend using as any instead of as never – there may be a linter warning about the former, but it more accurately describes what we're actually doing)

cleanup: sandbox.spy(),
addCleanup: sandbox.spy(),
};

beforeEach(function () {
eventListeners.clear();
});

const dataService = {} as never;
const atlasAiService = {} as never;
let store: ReturnType<typeof activatePlugin>['store'];
let deactivate: ReturnType<typeof activatePlugin>['deactivate'];

Expand Down Expand Up @@ -128,14 +155,15 @@ describe('Collection Tab Content store', function () {
dataService,
atlasAiService,
localAppRegistry,
collection: mockCollection as any,
workspaces: workspaces as any,
experimentationServices: experimentationServices as any,
connectionInfoRef: connectionInfoRef as any,
globalAppRegistry,
collection: mockCollection as never,
workspaces: workspaces as never,
experimentationServices: experimentationServices as never,
connectionInfoRef: connectionInfoRef as never,
logger,
preferences,
},
{ on() {}, cleanup() {}, addCleanup() {} } as any
mockCleanupHelper as never
));
await waitFor(() => {
expect(store.getState())
Expand All @@ -156,7 +184,7 @@ describe('Collection Tab Content store', function () {
const store = await configureStore(undefined, {
openCollectionWorkspaceSubtab,
});
store.dispatch(selectTab('Documents') as any);
store.dispatch(selectTab('Documents') as never);
expect(openCollectionWorkspaceSubtab).to.have.been.calledWith(
'workspace-tab-id',
'Documents'
Expand Down Expand Up @@ -206,7 +234,7 @@ describe('Collection Tab Content store', function () {
);

// Wait a bit to ensure assignment would have happened if it was going to
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(assignExperiment).to.not.have.been.called;
});

Expand All @@ -229,7 +257,7 @@ describe('Collection Tab Content store', function () {
);

// Wait a bit to ensure assignment would have happened if it was going to
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(assignExperiment).to.not.have.been.called;

// Store should still be functional
Expand Down Expand Up @@ -278,7 +306,7 @@ describe('Collection Tab Content store', function () {
);

// Wait a bit to ensure assignment would have happened if it was going to
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(assignExperiment).to.not.have.been.called;
});

Expand All @@ -296,7 +324,7 @@ describe('Collection Tab Content store', function () {
);

// Wait a bit to ensure assignment would have happened if it was going to
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(assignExperiment).to.not.have.been.called;
});
});
Expand Down Expand Up @@ -377,7 +405,7 @@ describe('Collection Tab Content store', function () {
});

// Wait a bit to ensure schema analysis would not have been called
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(analyzeCollectionSchemaStub).to.not.have.been.called;
});

Expand Down Expand Up @@ -428,7 +456,7 @@ describe('Collection Tab Content store', function () {
});

// Wait a bit to ensure schema analysis would not have been called
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(analyzeCollectionSchemaStub).to.not.have.been.called;
});

Expand All @@ -450,7 +478,7 @@ describe('Collection Tab Content store', function () {
});

// Wait a bit to ensure schema analysis would not have been called
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(analyzeCollectionSchemaStub).to.not.have.been.called;
});
});
Expand All @@ -470,12 +498,205 @@ describe('Collection Tab Content store', function () {
});

// Dispatch cancel action
store.dispatch(collectionTabModule.cancelSchemaAnalysis() as any);
store.dispatch(collectionTabModule.cancelSchemaAnalysis() as never);

// Verify the state is reset to initial
expect((store.getState() as any).schemaAnalysis.status).to.equal(
'initial'
expect(
(store.getState() as { schemaAnalysis: { status: string } })
.schemaAnalysis.status
).to.equal('initial');
});
});

describe('document-inserted event listener', function () {
it('should re-trigger schema analysis when document is inserted into current collection', async function () {
const getAssignment = sandbox.spy(() =>
Promise.resolve(
createMockAssignment(ExperimentTestGroup.mockDataGeneratorVariant)
)
);
const assignExperiment = sandbox.spy(() => Promise.resolve(null));

const store = await configureStore(
undefined,
undefined,
{ getAssignment, assignExperiment },
mockAtlasConnectionInfo,
undefined,
undefined,
{ ...defaultMetadata, isReadonly: false, isTimeSeries: false }
);

// Wait for initial schema analysis to complete
await waitFor(() => {
expect(analyzeCollectionSchemaStub).to.have.been.calledOnce;
});

// Reset the stub to track new calls
analyzeCollectionSchemaStub.resetHistory();

// Simulate the empty collection
store.dispatch({
type: 'compass-collection/SchemaAnalysisFailed',
error: new Error('No documents found'),
} as never);

// Trigger the document-inserted event listener
const documentInsertedListeners =
eventListeners.get('document-inserted') || [];
expect(documentInsertedListeners).to.have.length(1);

// Call the event listener with the event payload
documentInsertedListeners[0](
{
ns: defaultMetadata.namespace,
view: 'default',
mode: 'default',
multiple: false,
docs: [{ _id: 'test-doc-id', name: 'test' }],
},
{ connectionId: mockAtlasConnectionInfo.current.id }
);

// Wait for schema analysis to be re-triggered
await waitFor(() => {
expect(analyzeCollectionSchemaStub).to.have.been.calledOnce;
});
});

it('should not re-trigger schema analysis for different collection', async function () {
const getAssignment = sandbox.spy(() =>
Promise.resolve(
createMockAssignment(ExperimentTestGroup.mockDataGeneratorVariant)
)
);
const assignExperiment = sandbox.spy(() => Promise.resolve(null));

await configureStore(
undefined,
undefined,
{ getAssignment, assignExperiment },
mockAtlasConnectionInfo
);

// Wait for initial schema analysis to complete
await waitFor(() => {
expect(analyzeCollectionSchemaStub).to.have.been.calledOnce;
});

// Reset the stub to track new calls
analyzeCollectionSchemaStub.resetHistory();

// Trigger the document-inserted event listener with different collection
const documentInsertedListeners =
eventListeners.get('document-inserted') || [];
expect(documentInsertedListeners).to.have.length(1);

// Call the event listener with different collection namespace
documentInsertedListeners[0](
{
ns: 'different.collection',
view: 'default',
mode: 'default',
multiple: false,
docs: [{ _id: 'test-doc-id', name: 'test' }],
},
{ connectionId: mockAtlasConnectionInfo.current.id }
);

// Wait a bit to ensure schema analysis is not called
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(analyzeCollectionSchemaStub).to.not.have.been.called;
});

it('should not re-trigger schema analysis for different connection', async function () {
const getAssignment = sandbox.spy(() =>
Promise.resolve(
createMockAssignment(ExperimentTestGroup.mockDataGeneratorVariant)
)
);
const assignExperiment = sandbox.spy(() => Promise.resolve(null));

await configureStore(
undefined,
undefined,
{ getAssignment, assignExperiment },
mockAtlasConnectionInfo
);

// Wait for initial schema analysis to complete
await waitFor(() => {
expect(analyzeCollectionSchemaStub).to.have.been.calledOnce;
});

// Reset the stub to track new calls
analyzeCollectionSchemaStub.resetHistory();

// Trigger the document-inserted event listener with different connection
const documentInsertedListeners =
eventListeners.get('document-inserted') || [];
expect(documentInsertedListeners).to.have.length(1);

// Call the event listener with different connection ID
documentInsertedListeners[0](
{
ns: defaultMetadata.namespace,
view: 'default',
mode: 'default',
multiple: false,
docs: [{ _id: 'test-doc-id', name: 'test' }],
},
{ connectionId: 'different-connection-id' }
);

// Wait a bit to ensure schema analysis is not called
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(analyzeCollectionSchemaStub).to.not.have.been.called;
});

it('should not re-trigger schema analysis when user is not in experiment variant', async function () {
const getAssignment = sandbox.spy(() =>
Promise.resolve(
createMockAssignment(ExperimentTestGroup.mockDataGeneratorControl)
)
);
const assignExperiment = sandbox.spy(() => Promise.resolve(null));

await configureStore(
undefined,
undefined,
{ getAssignment, assignExperiment },
mockAtlasConnectionInfo
);

// Wait for initial assignment check
await waitFor(() => {
expect(getAssignment).to.have.been.calledOnce;
});

// Schema analysis should not have been called initially
expect(analyzeCollectionSchemaStub).to.not.have.been.called;

// Trigger the document-inserted event listener
const documentInsertedListeners =
eventListeners.get('document-inserted') || [];
expect(documentInsertedListeners).to.have.length(1);

// Call the event listener
documentInsertedListeners[0](
{
ns: defaultMetadata.namespace,
view: 'default',
mode: 'default',
multiple: false,
docs: [{ _id: 'test-doc-id', name: 'test' }],
},
{ connectionId: mockAtlasConnectionInfo.current.id }
);

// Wait a bit to ensure schema analysis is not called
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
expect(analyzeCollectionSchemaStub).to.not.have.been.called;
});
});
});
Loading
Loading