Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(schema): File download COMPASS-8704 #6740

Merged
merged 14 commits into from
Feb 27, 2025
31 changes: 30 additions & 1 deletion packages/compass-e2e-tests/helpers/compass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { inspect } from 'util';
import { ObjectId, EJSON } from 'bson';
import { promises as fs, rmdirSync } from 'fs';
import type Mocha from 'mocha';
import path from 'path';
import os from 'os';
import { execFile } from 'child_process';
import type { ExecFileOptions, ExecFileException } from 'child_process';
Expand Down Expand Up @@ -42,6 +41,8 @@ import {
ELECTRON_PATH,
} from './test-runner-paths';
import treeKill from 'tree-kill';
import { downloadPath } from './downloads';
import path from 'path';

const killAsync = async (pid: number, signal?: string) => {
return new Promise<void>((resolve, reject) => {
Expand Down Expand Up @@ -677,6 +678,13 @@ async function startCompassElectron(

try {
browser = (await remote(options)) as CompassBrowser;
// https://webdriver.io/docs/best-practices/file-download/#configuring-chromium-browser-downloads
const page = await browser.getPuppeteer();
const cdpSession = await page.target().createCDPSession();
await cdpSession.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: downloadPath,
});
Comment on lines +682 to +687
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just curious why it's different from what we do for the browser below where it's configured through chromeOptions. Asking because we do pass goog:chromeOptions to electron already and they seem to be applied correctly 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm.. this is how I understood the best practices docs, but let me try it out with just the options

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not out of the box, perhaps if we found an option to disable the 'save as' dialog

Copy link
Collaborator

Choose a reason for hiding this comment

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

Got it, interesting, thanks for checking!

} catch (err) {
debug('Failed to start remote webdriver session', {
error: (err as Error).stack,
Expand Down Expand Up @@ -755,6 +763,26 @@ export async function startBrowser(
runCounter++;
const { webdriverOptions, wdioOptions } = await processCommonOpts();

const browserCapabilities: Record<string, Record<string, unknown>> = {
chrome: {
'goog:chromeOptions': {
prefs: {
'download.default_directory': downloadPath,
},
},
},
firefox: {
'moz:firefoxOptions': {
prefs: {
'browser.download.dir': downloadPath,
'browser.download.folderList': 2,
'browser.download.manager.showWhenStarting': false,
'browser.helperApps.neverAsk.saveToDisk': '*/*',
},
},
},
};

// webdriverio removed RemoteOptions. It is now
// Capabilities.WebdriverIOConfig, but Capabilities is not exported
const options = {
Expand All @@ -763,6 +791,7 @@ export async function startBrowser(
...(context.browserVersion && {
browserVersion: context.browserVersion,
}),
...browserCapabilities[context.browserName],

// from https://github.com/webdriverio-community/wdio-electron-service/blob/32457f60382cb4970c37c7f0a19f2907aaa32443/packages/wdio-electron-service/src/launcher.ts#L102
'wdio:enforceWebDriverClassic': true,
Expand Down
33 changes: 33 additions & 0 deletions packages/compass-e2e-tests/helpers/downloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import path from 'path';
import fs from 'fs';

export const downloadPath = path.join(__dirname, 'downloads');

export const waitForFileDownload = async (
filename: string,
browser: WebdriverIO.Browser
): Promise<{
fileExists: boolean;
filePath: string;
}> => {
const filePath = `${downloadPath}/${filename}`;
await browser.waitUntil(
function () {
return fs.existsSync(filePath);
},
{ timeout: 10000, timeoutMsg: 'file not downloaded yet.' }
);

return { fileExists: fs.existsSync(filePath), filePath };
};

export const cleanUpDownloadedFile = (filename: string) => {
const filePath = `${downloadPath}/${filename}`;
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (err) {
console.error(`Error deleting file: ${(err as Error).message}`);
}
};
5 changes: 5 additions & 0 deletions packages/compass-e2e-tests/helpers/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,12 @@ export const AnalyzeSchemaButton = '[data-testid="analyze-schema-button"]';
export const ExportSchemaButton = '[data-testid="open-schema-export-button"]';
export const ExportSchemaFormatOptions =
'[data-testid="export-schema-format-type-box-group"]';
export const exportSchemaFormatOption = (
option: 'standardJSON' | 'mongoDBJSON' | 'extendedJSON'
) => `label[for="export-schema-format-${option}-button"]`;
export const ExportSchemaOutput = '[data-testid="export-schema-content"]';
export const ExportSchemaDownloadButton =
'[data-testid="schema-export-download-button"]';
export const SchemaFieldList = '[data-testid="schema-field-list"]';
export const AnalysisMessage =
'[data-testid="schema-content"] [data-testid="schema-analysis-message"]';
Expand Down
74 changes: 70 additions & 4 deletions packages/compass-e2e-tests/tests/collection-schema-tab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ import {
screenshotIfFailed,
skipForWeb,
DEFAULT_CONNECTION_NAME_1,
TEST_COMPASS_WEB,
} from '../helpers/compass';
import type { Compass } from '../helpers/compass';
import * as Selectors from '../helpers/selectors';
import {
createGeospatialCollection,
createNumbersCollection,
} from '../helpers/insert-data';
import {
cleanUpDownloadedFile,
waitForFileDownload,
} from '../helpers/downloads';
import { readFileSync } from 'fs';

const { expect } = chai;

Expand Down Expand Up @@ -111,12 +117,21 @@ describe('Collection schema tab', function () {

describe('with the enableExportSchema feature flag enabled', function () {
beforeEach(async function () {
// TODO(COMPASS-8819): remove web skip when defaulted true.
skipForWeb(this, "can't toggle features in compass-web");
await browser.setFeature('enableExportSchema', true);
if (!TEST_COMPASS_WEB)
await browser.setFeature('enableExportSchema', true);
});

const filename = 'schema-test-numbers-mongoDBJSON.json';

before(() => {
cleanUpDownloadedFile(filename);
});

it('shows an exported schema to copy', async function () {
after(() => {
cleanUpDownloadedFile(filename);
});

it('shows an exported schema to copy (standard JSON Schema)', async function () {
await browser.navigateToCollectionTab(
DEFAULT_CONNECTION_NAME_1,
'test',
Expand Down Expand Up @@ -157,6 +172,57 @@ describe('Collection schema tab', function () {
},
});
});

it('can download schema (MongoDB $jsonSchema)', async function () {
await browser.navigateToCollectionTab(
DEFAULT_CONNECTION_NAME_1,
'test',
'numbers',
'Schema'
);
await browser.clickVisible(Selectors.AnalyzeSchemaButton);

const element = browser.$(Selectors.SchemaFieldList);
await element.waitForDisplayed();

await browser.clickVisible(Selectors.ExportSchemaButton);

const exportModal = browser.$(Selectors.ExportSchemaFormatOptions);
await exportModal.waitForDisplayed();

await browser.clickVisible(
Selectors.exportSchemaFormatOption('mongoDBJSON')
);

const exportSchemaButton = browser.$(
Selectors.ExportSchemaDownloadButton
);
await exportSchemaButton.waitForEnabled();
await exportSchemaButton.click();

const { fileExists, filePath } = await waitForFileDownload(
filename,
browser
);
expect(fileExists).to.be.true;

const content = readFileSync(filePath, 'utf-8');
expect(JSON.parse(content)).to.deep.equal({
bsonType: 'object',
required: ['_id', 'i', 'j'],
properties: {
_id: {
bsonType: 'objectId',
},
i: {
bsonType: 'int',
},
j: {
bsonType: 'int',
},
},
});
});
});

it('analyzes the schema with a query');
Expand Down
16 changes: 13 additions & 3 deletions packages/compass-schema/src/components/export-schema-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ErrorSummary,
Label,
CancelLoader,
SpinLoader,
} from '@mongodb-js/compass-components';
import { CodemirrorMultilineEditor } from '@mongodb-js/compass-editor';

Expand All @@ -25,6 +26,7 @@ import {
trackSchemaExported,
type SchemaFormat,
type ExportStatus,
downloadSchema,
} from '../stores/schema-export-reducer';

const loaderStyles = css({
Expand Down Expand Up @@ -80,10 +82,12 @@ const ExportSchemaModal: React.FunctionComponent<{
resultId?: string;
exportFormat: SchemaFormat;
exportedSchema?: string;
filename?: string;
onCancelSchemaExport: () => void;
onChangeSchemaExportFormat: (format: SchemaFormat) => Promise<void>;
onClose: () => void;
onExportedSchemaCopied: () => void;
onSchemaDownload: () => void;
}> = ({
errorMessage,
exportStatus,
Expand All @@ -94,6 +98,7 @@ const ExportSchemaModal: React.FunctionComponent<{
onChangeSchemaExportFormat,
onClose,
onExportedSchemaCopied,
onSchemaDownload,
}) => {
const onFormatOptionSelected = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -178,10 +183,12 @@ const ExportSchemaModal: React.FunctionComponent<{
Cancel
</Button>
<Button
onClick={() => {
/* TODO(COMPASS-8704): download and track with trackSchemaExported */
}}
variant="primary"
isLoading={exportStatus === 'inprogress'}
loadingIndicator={<SpinLoader />}
disabled={!exportedSchema}
onClick={onSchemaDownload}
Copy link
Member

Choose a reason for hiding this comment

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

Are we planning on adding a visual indicator that the file is saving & saved? Right now the save dialogue closes and they're back to the export page which they would then cancel out of. Looking at the designs there is an in progress toast included:
https://www.figma.com/design/CBHJriBQZ2qxMxtnXFgDxJ/EXPO-3174-%3A-Export-Schema-%2B-Validation-Detection?node-id=1298-55009&t=MBxcSDens4RhwtPM-1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah! No, thanks for pointing that out, I forgot we had that in the designs. We don't have that control with the link approach as we'd have with file writing. The file download is handled by the browser - luckily it should be pretty much instantaneous. The part that might take more time is actually the stringifying, which we do after the conversion - so we already have indicators for that.
Should we close the export page after the click? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the File System API was more widely supported.. but for now it would be too much added complexity for something that should be quick.

Copy link
Member

@Anemy Anemy Feb 26, 2025

Choose a reason for hiding this comment

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

Closing the modal makes sense to me. It'll be a nicer ux once we have toasts.

data-testid="schema-export-download-button"
>
Export
</Button>
Expand All @@ -197,11 +204,14 @@ export default connect(
exportFormat: state.schemaExport.exportFormat,
isOpen: state.schemaExport.isOpen,
exportedSchema: state.schemaExport.exportedSchema,
filename: state.schemaExport.filename,
}),
{
onExportedSchemaCopied: trackSchemaExported,
onExportedSchema: trackSchemaExported,
onCancelSchemaExport: cancelExportSchema,
onChangeSchemaExportFormat: changeExportSchemaFormat,
onClose: closeExportSchema,
onSchemaDownload: downloadSchema,
}
)(ExportSchemaModal);
Loading
Loading