Skip to content

Commit 5716285

Browse files
committed
control modal steps with redux
1 parent 21ef538 commit 5716285

File tree

9 files changed

+377
-274
lines changed

9 files changed

+377
-274
lines changed
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', 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 });

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@ import { connect } from 'react-redux';
33

44
import {
55
css,
6+
Button,
7+
ButtonVariant,
68
ModalBody,
79
ModalHeader,
8-
spacing,
9-
} from '@mongodb-js/compass-components';
10-
11-
import {
12-
Button,
1310
Modal,
1411
ModalFooter,
15-
ButtonVariant,
12+
spacing,
1613
} from '@mongodb-js/compass-components';
14+
1715
import { MockDataGeneratorStep } from './types';
1816
import { StepButtonLabelMap } from './constants';
1917
import type { CollectionState } from '../../modules/collection-tab';
2018
import {
2119
mockDataGeneratorModalClosed,
2220
mockDataGeneratorNextButtonClicked,
21+
generateFakerMappings,
2322
mockDataGeneratorPreviousButtonClicked,
2423
} from '../../modules/collection-tab';
24+
import { default as SchemaConfirmationScreen } from './raw-schema-confirmation';
25+
import FakerSchemaEditor from './faker-schema-editor';
2526

2627
const footerStyles = css`
2728
flex-direction: row;
@@ -39,6 +40,7 @@ interface Props {
3940
onClose: () => void;
4041
currentStep: MockDataGeneratorStep;
4142
onNextStep: () => void;
43+
onConfirmSchema: () => Promise<void>;
4244
onPreviousStep: () => void;
4345
}
4446

@@ -47,10 +49,28 @@ const MockDataGeneratorModal = ({
4749
onClose,
4850
currentStep,
4951
onNextStep,
52+
onConfirmSchema,
5053
onPreviousStep,
5154
}: Props) => {
52-
const handleNextClick =
53-
currentStep === MockDataGeneratorStep.GENERATE_DATA ? onClose : onNextStep;
55+
const handleNextClick = () => {
56+
if (currentStep === MockDataGeneratorStep.GENERATE_DATA) {
57+
onClose();
58+
} else if (currentStep === MockDataGeneratorStep.SCHEMA_CONFIRMATION) {
59+
void onConfirmSchema();
60+
} else {
61+
onNextStep();
62+
}
63+
};
64+
65+
let stepContent: React.ReactNode;
66+
67+
if (currentStep === MockDataGeneratorStep.SCHEMA_CONFIRMATION) {
68+
stepContent = <SchemaConfirmationScreen />;
69+
}
70+
71+
if (currentStep === MockDataGeneratorStep.SCHEMA_EDITOR) {
72+
stepContent = <FakerSchemaEditor />;
73+
}
5474

5575
return (
5676
<Modal
@@ -64,7 +84,7 @@ const MockDataGeneratorModal = ({
6484
>
6585
<ModalHeader title="Generate Mock Data" />
6686
<ModalBody>
67-
{/* TODO: Render actual step content here based on currentStep. (CLOUDP-333851) */}
87+
{stepContent}
6888
<div data-testid={`generate-mock-data-step-${currentStep}`} />
6989
</ModalBody>
7090
<ModalFooter className={footerStyles}>
@@ -97,8 +117,8 @@ const mapStateToProps = (state: CollectionState) => ({
97117
const ConnectedMockDataGeneratorModal = connect(mapStateToProps, {
98118
onClose: mockDataGeneratorModalClosed,
99119
onNextStep: mockDataGeneratorNextButtonClicked,
120+
onConfirmSchema: generateFakerMappings,
100121
onPreviousStep: mockDataGeneratorPreviousButtonClicked,
101122
})(MockDataGeneratorModal);
102123

103124
export default ConnectedMockDataGeneratorModal;
104-
export { MockDataGeneratorModal as UnconnectedMockDataGeneratorModal };

0 commit comments

Comments
 (0)