Skip to content

Commit 8a5b385

Browse files
authored
feat(compass-collection): integrate llm call into mock data generator modal CLOUDP-333850 (#7251)
1 parent d54cea5 commit 8a5b385

File tree

13 files changed

+598
-83
lines changed

13 files changed

+598
-83
lines changed

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-collection/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@
5555
"@mongodb-js/compass-generative-ai": "^0.51.0",
5656
"@mongodb-js/compass-logging": "^1.7.12",
5757
"@mongodb-js/compass-telemetry": "^1.14.0",
58+
"@mongodb-js/compass-utils": "^0.9.11",
5859
"@mongodb-js/compass-workspaces": "^0.52.0",
5960
"@mongodb-js/connection-info": "^0.17.2",
6061
"@mongodb-js/mongodb-constants": "^0.14.0",
62+
"bson": "^6.10.1",
6163
"compass-preferences-model": "^2.51.0",
6264
"hadron-document": "^8.9.6",
6365
"mongodb": "^6.19.0",
@@ -67,8 +69,7 @@
6769
"react": "^17.0.2",
6870
"react-redux": "^8.1.3",
6971
"redux": "^4.2.1",
70-
"redux-thunk": "^2.4.2",
71-
"bson": "^6.10.1"
72+
"redux-thunk": "^2.4.2"
7273
},
7374
"devDependencies": {
7475
"@mongodb-js/eslint-config-compass": "^1.4.7",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
3+
// TODO: More to come from CLOUDP-333853, CLOUDP-333854
4+
const FakerSchemaEditor = () => {
5+
return (
6+
<div data-testid="faker-schema-editor">
7+
Schema Editor Content Placeholder
8+
</div>
9+
);
10+
};
11+
12+
export default FakerSchemaEditor;

packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx

Lines changed: 203 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,239 @@
11
import { expect } from 'chai';
22
import React from 'react';
3-
import { render, screen } from '@mongodb-js/testing-library-compass';
4-
import Sinon from 'sinon';
5-
import { UnconnectedMockDataGeneratorModal as MockDataGeneratorModal } from './mock-data-generator-modal';
3+
import {
4+
screen,
5+
render,
6+
waitFor,
7+
userEvent,
8+
} from '@mongodb-js/testing-library-compass';
9+
import { Provider } from 'react-redux';
10+
import { createStore, applyMiddleware } from 'redux';
11+
import thunk from 'redux-thunk';
12+
import MockDataGeneratorModal from './mock-data-generator-modal';
613
import { MockDataGeneratorStep } from './types';
714
import { StepButtonLabelMap } from './constants';
15+
import type { CollectionState } from '../../modules/collection-tab';
16+
import { default as collectionTabReducer } from '../../modules/collection-tab';
817

918
describe('MockDataGeneratorModal', () => {
10-
const sandbox = Sinon.createSandbox();
11-
let onClose: Sinon.SinonSpy;
12-
13-
beforeEach(() => {
14-
onClose = sandbox.spy();
15-
});
16-
17-
afterEach(() => {
18-
sandbox.restore();
19-
});
20-
21-
const onNextStep = Sinon.stub();
22-
const onPreviousStep = Sinon.stub();
23-
2419
function renderModal({
2520
isOpen = true,
2621
currentStep = MockDataGeneratorStep.SCHEMA_CONFIRMATION,
22+
mockServices = createMockServices(),
2723
} = {}) {
24+
const initialState: CollectionState = {
25+
workspaceTabId: 'test-workspace-tab-id',
26+
namespace: 'test.collection',
27+
metadata: null,
28+
schemaAnalysis: {
29+
status: 'complete',
30+
processedSchema: {
31+
name: {
32+
type: 'String',
33+
probability: 1.0,
34+
sample_values: ['John', 'Jane'],
35+
},
36+
},
37+
sampleDocument: { name: 'John' },
38+
schemaMetadata: { maxNestingDepth: 1, validationRules: null },
39+
},
40+
fakerSchemaGeneration: {
41+
status: 'idle',
42+
},
43+
mockDataGenerator: {
44+
isModalOpen: isOpen,
45+
currentStep: currentStep,
46+
},
47+
};
48+
49+
const store = createStore(
50+
collectionTabReducer,
51+
initialState,
52+
applyMiddleware(thunk.withExtraArgument(mockServices))
53+
);
54+
2855
return render(
29-
<MockDataGeneratorModal
30-
isOpen={isOpen}
31-
onClose={onClose}
32-
currentStep={currentStep}
33-
onNextStep={onNextStep}
34-
onPreviousStep={onPreviousStep}
35-
/>
56+
<Provider store={store}>
57+
<MockDataGeneratorModal />
58+
</Provider>
3659
);
3760
}
3861

39-
it('renders the modal when isOpen is true', () => {
40-
renderModal();
62+
function createMockServices() {
63+
return {
64+
dataService: {},
65+
atlasAiService: {
66+
getMockDataSchema: () => {
67+
return Promise.resolve({
68+
contents: {
69+
fields: [],
70+
},
71+
});
72+
},
73+
},
74+
workspaces: {},
75+
localAppRegistry: {},
76+
experimentationServices: {},
77+
connectionInfoRef: { current: {} },
78+
logger: {
79+
log: {
80+
warn: () => {},
81+
error: () => {},
82+
},
83+
debug: () => {},
84+
mongoLogId: () => 'mock-id',
85+
},
86+
preferences: { getPreferences: () => ({}) },
87+
fakerSchemaGenerationAbortControllerRef: { current: undefined },
88+
};
89+
}
4190

42-
expect(screen.getByTestId('generate-mock-data-modal')).to.exist;
43-
});
91+
describe('generally', () => {
92+
it('renders the modal when isOpen is true', () => {
93+
renderModal();
4494

45-
it('does not render the modal when isOpen is false', () => {
46-
renderModal({ isOpen: false });
95+
expect(screen.getByTestId('generate-mock-data-modal')).to.exist;
96+
});
4797

48-
expect(screen.queryByTestId('generate-mock-data-modal')).to.not.exist;
49-
});
98+
it('does not render the modal when isOpen is false', () => {
99+
renderModal({ isOpen: false });
50100

51-
it('calls onClose when the modal is closed', () => {
52-
renderModal();
101+
expect(screen.queryByTestId('generate-mock-data-modal')).to.not.exist;
102+
});
53103

54-
screen.getByLabelText('Close modal').click();
104+
it('closes the modal when the close button is clicked', async () => {
105+
renderModal();
55106

56-
expect(onClose.calledOnce).to.be.true;
57-
});
107+
expect(screen.getByTestId('generate-mock-data-modal')).to.exist;
108+
userEvent.click(screen.getByLabelText('Close modal'));
109+
await waitFor(
110+
() =>
111+
expect(screen.queryByTestId('generate-mock-data-modal')).to.not.exist
112+
);
113+
});
58114

59-
it('calls onClose when the cancel button is clicked', () => {
60-
renderModal();
115+
it('closes the modal when the cancel button is clicked', async () => {
116+
renderModal();
61117

62-
screen.getByText('Cancel').click();
118+
expect(screen.getByTestId('generate-mock-data-modal')).to.exist;
119+
userEvent.click(screen.getByText('Cancel'));
120+
await waitFor(
121+
() =>
122+
expect(screen.queryByTestId('generate-mock-data-modal')).to.not.exist
123+
);
124+
});
125+
126+
function createMockServicesWithSlowAiRequest() {
127+
let abortSignalReceived = false;
128+
let rejectPromise: (reason?: any) => void;
129+
const rejectedPromise = new Promise((_resolve, reject) => {
130+
rejectPromise = reject;
131+
});
132+
133+
const baseMockServices = createMockServices();
134+
135+
const mockAiService = {
136+
...baseMockServices.atlasAiService,
137+
getMockDataSchema: (request: any) => {
138+
if (request?.signal) {
139+
request.signal.addEventListener('abort', () => {
140+
abortSignalReceived = true;
141+
rejectPromise(new Error('Request aborted'));
142+
});
143+
}
144+
return rejectedPromise;
145+
},
146+
getAbortSignalReceived: () => abortSignalReceived,
147+
};
148+
149+
return {
150+
...baseMockServices,
151+
atlasAiService: mockAiService,
152+
};
153+
}
154+
155+
it('cancels in-flight faker mapping requests when the cancel button is clicked', async () => {
156+
const mockServices = createMockServicesWithSlowAiRequest();
157+
renderModal({ mockServices: mockServices as any });
158+
159+
expect(screen.getByTestId('raw-schema-confirmation')).to.exist;
160+
userEvent.click(screen.getByText('Confirm'));
161+
162+
await waitFor(() => {
163+
expect(screen.getByTestId('faker-schema-editor')).to.exist;
164+
});
63165

64-
expect(onClose.calledOnce).to.be.true;
166+
userEvent.click(screen.getByText('Cancel'));
167+
168+
expect(mockServices.atlasAiService.getAbortSignalReceived()).to.be.true;
169+
});
170+
171+
it('cancels in-flight faker mapping requests when the back button is clicked after schema confirmation', async () => {
172+
const mockServices = createMockServicesWithSlowAiRequest();
173+
renderModal({ mockServices: mockServices as any });
174+
175+
expect(screen.getByTestId('raw-schema-confirmation')).to.exist;
176+
userEvent.click(screen.getByText('Confirm'));
177+
178+
await waitFor(() => {
179+
expect(screen.getByTestId('faker-schema-editor')).to.exist;
180+
});
181+
182+
userEvent.click(screen.getByText('Back'));
183+
184+
expect(mockServices.atlasAiService.getAbortSignalReceived()).to.be.true;
185+
});
65186
});
66187

67-
it('disables the Back button on the first step', () => {
68-
renderModal();
188+
describe('on the schema confirmation step', () => {
189+
it('disables the Back button', () => {
190+
renderModal();
191+
192+
expect(
193+
screen
194+
.getByRole('button', { name: 'Back' })
195+
.getAttribute('aria-disabled')
196+
).to.equal('true');
197+
});
198+
199+
it('renders the faker schema editor when the confirm button is clicked', async () => {
200+
renderModal();
201+
202+
expect(screen.getByTestId('raw-schema-confirmation')).to.exist;
203+
expect(screen.queryByTestId('faker-schema-editor')).to.not.exist;
204+
userEvent.click(screen.getByText('Confirm'));
205+
await waitFor(() => {
206+
expect(screen.queryByTestId('raw-schema-confirmation')).to.not.exist;
207+
expect(screen.getByTestId('faker-schema-editor')).to.exist;
208+
});
209+
});
210+
211+
it('stays on the current step when an error is encountered during faker schema generation', async () => {
212+
const mockServices = createMockServices();
213+
mockServices.atlasAiService.getMockDataSchema = () =>
214+
Promise.reject('faker schema generation failed');
215+
renderModal({ mockServices });
216+
217+
expect(screen.getByTestId('raw-schema-confirmation')).to.exist;
218+
expect(screen.queryByTestId('faker-schema-editor')).to.not.exist;
219+
userEvent.click(screen.getByText('Confirm'));
220+
await waitFor(() => {
221+
expect(screen.getByTestId('raw-schema-confirmation')).to.exist;
222+
expect(screen.queryByTestId('faker-schema-editor')).to.not.exist;
223+
});
224+
225+
// todo: assert a user-friendly error is displayed (CLOUDP-333852)
226+
});
69227

70-
expect(
71-
screen.getByRole('button', { name: 'Back' }).getAttribute('aria-disabled')
72-
).to.equal('true');
228+
// todo: assert that closing then re-opening the modal after an LLM err removes the err message
73229
});
74230

75231
describe('when rendering the modal in a specific step', () => {
76232
const steps = Object.keys(
77233
StepButtonLabelMap
78234
) as unknown as MockDataGeneratorStep[];
79235

236+
// note: these tests can be removed after every modal step is implemented
80237
steps.forEach((currentStep) => {
81238
it(`renders the button with the correct label when the user is in step "${currentStep}"`, () => {
82239
renderModal({ currentStep });

0 commit comments

Comments
 (0)