Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion packages/compass-e2e-tests/helpers/commands/set-validation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CompassBrowser } from '../compass-browser';
import * as Selectors from '../selectors';

export async function setValidation(
export async function setValidationWithinValidationTab(
browser: CompassBrowser,
value: string
): Promise<void> {
Expand Down Expand Up @@ -32,3 +32,29 @@ export async function setValidation(
// replaced
await browser.pause(2000);
}

export async function setValidation(
browser: CompassBrowser,
{
connectionName,
database,
collection,
validator,
}: {
connectionName: string;
database: string;
collection: string;
validator: string;
}
): Promise<void> {
await browser.navigateToCollectionTab(
connectionName,
database,
collection,
'Validation'
);
await browser.clickVisible(Selectors.AddRuleButton);
const element = browser.$(Selectors.ValidationEditor);
await element.waitForDisplayed();
await browser.setValidationWithinValidationTab(validator);
}
4 changes: 4 additions & 0 deletions packages/compass-e2e-tests/helpers/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,10 @@ export const ImportAnalyzeError =
export const ImportConfirm =
'[data-testid="import-modal"] [data-testid="import-button"]';
export const ImportToast = '[data-testid="toast-import-toast"]';
export const ImportToastErrorDetailsBtn =
'[data-testid="toast-import-toast"] [data-testid="import-error-details-button"]';
export const ImportErrorDetailsModal =
'[data-testid="import-error-details-modal"]';
export const ImportToastAbort = '[data-testid="toast-action-stop"]';
export const ImportFieldLabel =
'[data-testid="import-modal"] .import-field-label';
Expand Down
16 changes: 6 additions & 10 deletions packages/compass-e2e-tests/tests/collection-documents-tab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,16 +691,12 @@ FindIterable<Document> result = collection.find(filter);`);
const REQUIRE_PHONE_VALIDATOR =
'{ $jsonSchema: { bsonType: "object", required: [ "phone" ] } }';
beforeEach(async function () {
await browser.navigateToCollectionTab(
DEFAULT_CONNECTION_NAME_1,
'test',
'numbers',
'Validation'
);
await browser.clickVisible(Selectors.AddRuleButton);
const element = browser.$(Selectors.ValidationEditor);
await element.waitForDisplayed();
await browser.setValidation(REQUIRE_PHONE_VALIDATOR);
await browser.setValidation({
connectionName: DEFAULT_CONNECTION_NAME_1,
database: 'test',
collection: 'numbers',
validator: REQUIRE_PHONE_VALIDATOR,
});
});

it('Shows error info when inserting', async function () {
Expand Down
133 changes: 133 additions & 0 deletions packages/compass-e2e-tests/tests/collection-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,139 @@ describe('Collection import', function () {
await toastElement.waitForDisplayed({ reverse: true });
});

context('with validation', function () {
beforeEach(async function () {
const FAILING_VALIDATOR =
'{ $jsonSchema: { bsonType: "object", required: [ "abcdefgh" ] } }';
await browser.setValidation({
connectionName: DEFAULT_CONNECTION_NAME_1,
database: 'test',
collection: 'extended-json-file',
validator: FAILING_VALIDATOR,
});
});

afterEach(async function () {
await browser.navigateWithinCurrentCollectionTabs('Validation');
await browser.setValidationWithinValidationTab('{}');
});

it('with JSON + abort on error checked, it displays a validation error with details', async function () {
const jsonPath = path.resolve(
__dirname,
'..',
'fixtures',
'three-documents.json'
);

await browser.navigateWithinCurrentCollectionTabs('Documents');

// open the import modal
await browser.clickVisible(Selectors.AddDataButton);
const insertDocumentOption = browser.$(Selectors.ImportFileOption);
await insertDocumentOption.waitForDisplayed();
await browser.clickVisible(Selectors.ImportFileOption);

// Select the file.
await browser.selectFile(Selectors.ImportFileInput, jsonPath);
// Wait for the modal to appear.
const importModal = browser.$(Selectors.ImportModal);
await importModal.waitForDisplayed();

// Click the stop on errors checkbox.
const stopOnErrorsCheckbox = browser.$(
Selectors.ImportStopOnErrorsCheckbox
);
const stopOnErrorsLabel = stopOnErrorsCheckbox.parentElement();
await stopOnErrorsLabel.click();

// Confirm import.
await browser.clickVisible(Selectors.ImportConfirm);

// Wait for the modal to go away.
await importModal.waitForDisplayed({ reverse: true });

// Wait for the error toast to appear
const toastElement = browser.$(Selectors.ImportToast);
await toastElement.waitForDisplayed();
const errorText = await toastElement.getText();
expect(errorText).to.include('Document failed validation');

// Visit error details
await browser.clickVisible(Selectors.ImportToastErrorDetailsBtn);
const errorDetailsModal = browser.$(Selectors.ImportErrorDetailsModal);
await errorDetailsModal.waitForDisplayed();
expect(await errorDetailsModal.getText()).to.include(
'schemaRulesNotSatisfied'
);
await browser.clickVisible(Selectors.ErrorDetailsCloseButton);

// Close the toast
await browser
.$(Selectors.closeToastButton(Selectors.ImportToast))
.waitForDisplayed();
await browser.clickVisible(
Selectors.closeToastButton(Selectors.ImportToast)
);
await toastElement.waitForDisplayed({ reverse: true });
});

it('with CSV + abort on error unchecked, it includes the details in a file', async function () {
const filename = 'array-documents.csv';
const csvPath = path.resolve(__dirname, '..', 'fixtures', filename);

await browser.navigateWithinCurrentCollectionTabs('Documents');

// open the import modal
await browser.clickVisible(Selectors.AddDataButton);
const insertDocumentOption = browser.$(Selectors.ImportFileOption);
await insertDocumentOption.waitForDisplayed();
await browser.clickVisible(Selectors.ImportFileOption);

// Select the file.
await browser.selectFile(Selectors.ImportFileInput, csvPath);
// Wait for the modal to appear.
const importModal = browser.$(Selectors.ImportModal);
await importModal.waitForDisplayed();

// Confirm import.
await browser.clickVisible(Selectors.ImportConfirm);

// Wait for the modal to go away.
await importModal.waitForDisplayed({ reverse: true });

// Wait for the error toast to appear
const toastElement = browser.$(Selectors.ImportToast);
await toastElement.waitForDisplayed();
const errorText = await toastElement.getText();
console.log({ errorText });
expect(errorText).to.include('Document failed validation');
expect(errorText).to.include('VIEW LOG');

// Find the log file
const logFilePath = path.resolve(
compass.userDataPath || '',
compass.appName || '',
'ImportErrorLogs',
`import-${filename}.log`
);
await expect(fs.stat(logFilePath)).to.not.be.rejected;

// Check the log file contents for 3 errors.
const logFileContent = await fs.readFile(logFilePath, 'utf-8');
expect(logFileContent.includes('schemaRulesNotSatisfied:'));

// Close the toast
await browser
.$(Selectors.closeToastButton(Selectors.ImportToast))
.waitForDisplayed();
await browser.clickVisible(
Selectors.closeToastButton(Selectors.ImportToast)
);
await toastElement.waitForDisplayed({ reverse: true });
});
});

it('supports CSV files', async function () {
const csvPath = path.resolve(__dirname, '..', 'fixtures', 'listings.csv');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('Collection validation tab', function () {
const element = browser.$(Selectors.ValidationEditor);
await element.waitForDisplayed();

await browser.setValidation(validation);
await browser.setValidationWithinValidationTab(validation);
}

context('when the schema validation is set or modified', function () {
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('Collection validation tab', function () {
// Reset the validation again to make everything valid for future tests

// the automatic indentation and brackets makes multi-line values very fiddly here
await browser.setValidation(PASSING_VALIDATOR);
await browser.setValidationWithinValidationTab(PASSING_VALIDATOR);
await browser.clickVisible(Selectors.ValidationLoadMatchingDocumentsBtn);
await browser.clickVisible(
Selectors.ValidationLoadNotMatchingDocumentsBtn
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import {
render,
screen,
userEvent,
waitFor,
} from '@mongodb-js/testing-library-compass';
import { expect } from 'chai';
import { configureStore } from '../stores/import-store';
import { Provider } from 'react-redux';

import { createNoopLogger } from '@mongodb-js/compass-logging/provider';
import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider';
import ImportErrorDetailsModal from './import-error-details-modal';

function renderModal(importState: any = {}) {
// TODO: mutating state directly doesn't guarantee that we are testing the
// component in a state that can actually be achieved when actions are emitted
// on the store. Refactor this to either test unconnected component, or to
// not mutate state directly for tests
const store = configureStore({
dataService: {},
globalAppRegistry: {},
logger: createNoopLogger(),
track: createNoopTrack(),
connections: {
getConnectionById: () => ({ info: { id: 'TEST' } }),
},
} as any);
const state = store.getState();
state.import = {
...state.import,
...importState,
};
const renderResult = render(
<Provider store={store}>
<ImportErrorDetailsModal />
</Provider>
);
return { renderResult, store };
}

describe('ImportErrorDetailsModal Component', function () {
context('When import error details are open', function () {
const errorDetails = { details: 'abc' };

beforeEach(function () {
renderModal({
errorDetails: {
isOpen: true,
details: errorDetails,
},
});
});

it('Should render error details and be closable', async function () {
const codeDetails = await screen.findByTestId('error-details-json');
expect(codeDetails).to.be.visible;
expect(JSON.parse(codeDetails.textContent || '')).to.deep.equal(
errorDetails
);

const closeBtn = await screen.findByRole('button', { name: 'Close' });
expect(closeBtn).to.be.visible;

userEvent.click(closeBtn);
await waitFor(() => {
expect(screen.queryByTestId('import-error-details-modal')).not.to.exist;
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { ErrorDetailsModal } from '@mongodb-js/compass-components';
import { connect } from 'react-redux';
import type { RootImportState } from '../stores/import-store';
import { onErrorDetailsClose } from '../modules/import';

const ImportErrorDetailsModal: React.FunctionComponent<{
isOpen: boolean;
errorDetails?: Record<string, unknown>;
onClose: () => void;
}> = ({ isOpen, errorDetails, onClose }) => {
return (
<ErrorDetailsModal
closeAction="close"
open={isOpen}
details={errorDetails}
onClose={onClose}
data-testid="import-error-details-modal"
/>
);
};

const ConnectedImportErrorDetailsModal = connect(
(state: RootImportState) => ({
isOpen: state.import.errorDetails.isOpen,
errorDetails: state.import.errorDetails.details,
}),
{
onClose: onErrorDetailsClose,
}
)(ImportErrorDetailsModal);

export default ConnectedImportErrorDetailsModal;
20 changes: 18 additions & 2 deletions packages/compass-import-export/src/components/import-toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import {
Body,
css,
Link,
openToast,
ToastBody,
} from '@mongodb-js/compass-components';
Expand Down Expand Up @@ -221,10 +222,25 @@ export function showCancelledToast({
});
}

export function showFailedToast(err: Error | undefined) {
export function showFailedToast(
err: Error | undefined,
showErrorDetails?: () => void
) {
openToast(importToastId, {
title: 'Failed to import with the following error:',
description: err?.message,
description: (
<>
{err?.message}&nbsp;
{showErrorDetails && (
<Link
onClick={showErrorDetails}
Copy link
Member

Choose a reason for hiding this comment

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

Can we close those toast when the user clicks to see the error? We currently will keep showing the toast when the modal is open, but the dismiss is not clickable as I the modal is capturing all of the clicks. I think there's a similar issue with the toast on connectivity issues, not the end of the world if it's not easily feasible, a nice to have.
Screenshot 2025-03-13 at 12 30 28 PM

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed!

data-testid="import-error-details-button"
>
View error details
</Link>
)}
</>
),
variant: 'warning',
});
}
2 changes: 2 additions & 0 deletions packages/compass-import-export/src/import-plugin.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from 'react';
import ImportModal from './components/import-modal';
import ImportInProgressModal from './components/import-in-progress-modal';
import ImportErrorDetailsModal from './components/import-error-details-modal';

function ImportPlugin() {
return (
<>
<ImportModal />
<ImportInProgressModal />
<ImportErrorDetailsModal />
</>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/compass-import-export/src/import/import-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type ErrorJSON = {
index?: number;
code?: string | number;
op?: any;
errorInfo?: Document;
errInfo?: Document;
numErrors?: number;
};

Expand Down
Loading
Loading