Skip to content
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
Collaborator 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
Collaborator 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
68 changes: 67 additions & 1 deletion packages/compass-e2e-tests/tests/collection-schema-tab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import {
createGeospatialCollection,
createNumbersCollection,
} from '../helpers/insert-data';
import {
cleanUpDownloadedFile,
waitForFileDownload,
} from '../helpers/downloads';
import { readFileSync } from 'fs';

const { expect } = chai;

Expand Down Expand Up @@ -116,7 +121,17 @@ describe('Collection schema tab', function () {
await browser.setFeature('enableExportSchema', true);
});

it('shows an exported schema to copy', async function () {
const filename = 'schema-test-numbers-mongoDBJSON.json';

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

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
38 changes: 35 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,
trackSchemaExportFailed,
} from '../stores/schema-export-reducer';

const loaderStyles = css({
Expand Down Expand Up @@ -80,20 +82,26 @@ const ExportSchemaModal: React.FunctionComponent<{
resultId?: string;
exportFormat: SchemaFormat;
exportedSchema?: string;
filename?: string;
onCancelSchemaExport: () => void;
onChangeSchemaExportFormat: (format: SchemaFormat) => Promise<void>;
onClose: () => void;
onExportedSchemaCopied: () => void;
onExportedSchema: () => void;
onSchemaExportFailed: (stage: string) => void;
}> = ({
errorMessage,
exportStatus,
isOpen,
exportFormat,
exportedSchema,
filename,
onCancelSchemaExport,
onChangeSchemaExportFormat,
onClose,
onExportedSchemaCopied,
onExportedSchema,
onSchemaExportFailed,
}) => {
const onFormatOptionSelected = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
Expand All @@ -104,6 +112,25 @@ const ExportSchemaModal: React.FunctionComponent<{
[onChangeSchemaExportFormat]
);

const handleSchemaDownload = useCallback(() => {
try {
if (!exportedSchema) return;
const blob = new Blob([exportedSchema], {
type: 'application/json',
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename || 'export.json';
link.click();
window.URL.revokeObjectURL(url);
onExportedSchema();
} catch (error) {
onSchemaExportFailed('download button clicked');
throw error;
}
}, [exportedSchema, filename, onSchemaExportFailed, onExportedSchema]);

return (
<Modal open={isOpen} setOpen={onClose}>
<ModalHeader title="Export Schema" />
Expand Down Expand Up @@ -178,10 +205,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={handleSchemaDownload}
data-testid="schema-export-download-button"
>
Export
</Button>
Expand All @@ -197,9 +226,12 @@ export default connect(
exportFormat: state.schemaExport.exportFormat,
isOpen: state.schemaExport.isOpen,
exportedSchema: state.schemaExport.exportedSchema,
filename: state.schemaExport.filename,
}),
{
onExportedSchemaCopied: trackSchemaExported,
onExportedSchema: trackSchemaExported,
onSchemaExportFailed: trackSchemaExportFailed,
onCancelSchemaExport: cancelExportSchema,
onChangeSchemaExportFormat: changeExportSchemaFormat,
onClose: closeExportSchema,
Expand Down
31 changes: 30 additions & 1 deletion packages/compass-schema/src/stores/schema-export-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type SchemaExportState = {
exportFormat: SchemaFormat;
errorMessage?: string;
exportStatus: ExportStatus;
filename?: string;
};

const defaultSchemaFormat: SchemaFormat = 'standardJSON';
Expand All @@ -52,6 +53,7 @@ export const enum SchemaExportActions {
changeExportSchemaFormatComplete = 'schema-service/schema-export/changeExportSchemaFormatComplete',
changeExportSchemaFormatError = 'schema-service/schema-export/changeExportSchemaFormatError',
cancelExportSchema = 'schema-service/schema-export/cancelExportSchema',
schemaDownloadReady = 'schema-service/schema-export/schemaDownloadReady',
}

export type OpenExportSchemaAction = {
Expand Down Expand Up @@ -103,6 +105,7 @@ export type ChangeExportSchemaFormatErroredAction = {
export type ChangeExportSchemaFormatCompletedAction = {
type: SchemaExportActions.changeExportSchemaFormatComplete;
exportedSchema: string;
filename: string;
};

export const cancelExportSchema = (): SchemaThunkAction<
Expand Down Expand Up @@ -150,6 +153,24 @@ async function getSchemaByFormat({
return JSON.stringify(schema, null, 2);
}

export const trackSchemaExportFailed = (
stage: string
): SchemaThunkAction<void> => {
return (dispatch, getState, { track, connectionInfoRef }) => {
const { exportedSchema, exportFormat } = getState().schemaExport;
track(
'Schema Export Failed',
{
has_schema: !!exportedSchema,
schema_length: exportedSchema?.length || 0,
format: exportFormat,
stage,
},
connectionInfoRef.current
);
};
};

const _trackSchemaExported = ({
schema,
source,
Expand Down Expand Up @@ -212,7 +233,7 @@ export const changeExportSchemaFormat = (
return async (
dispatch,
getState,
{ logger: { log }, exportAbortControllerRef, schemaAccessorRef }
{ logger: { log }, exportAbortControllerRef, schemaAccessorRef, namespace }
) => {
// If we're already in progress we abort their current operation.
exportAbortControllerRef.current?.abort();
Expand Down Expand Up @@ -263,6 +284,7 @@ export const changeExportSchemaFormat = (
type: SchemaExportActions.changeExportSchemaFormatError,
errorMessage: (err as Error).message,
});
dispatch(trackSchemaExportFailed('switching format'));
return;
}

Expand All @@ -280,9 +302,15 @@ export const changeExportSchemaFormat = (
}
);

const filename = `schema-${namespace.replace(
'.',
'-'
)}-${exportFormat}.json`;

dispatch({
type: SchemaExportActions.changeExportSchemaFormatComplete,
exportedSchema,
filename,
});
};
};
Expand Down Expand Up @@ -386,6 +414,7 @@ export const schemaExportReducer: Reducer<SchemaExportState, Action> = (
return {
...state,
exportedSchema: action.exportedSchema,
filename: action.filename,
exportStatus: 'complete',
};
}
Expand Down
Loading
Loading