Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
108 changes: 74 additions & 34 deletions packages/compass-e2e-tests/helpers/compass.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { inspect } from 'util';
import { ObjectId, EJSON } from 'bson';
import { promises as fs } from 'fs';
import { promises as fs, rmdirSync } from 'fs';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a specific reason that this method needs to be synchronous?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

cleanup() in index.ts is synchronous and I need to call it there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See

function cleanup() {
keychain.reset();
const disableStartStop = process.argv.includes('--disable-start-stop');
if (!disableStartStop) {
debug('Stopping MongoDB server and cleaning up server data');
try {
crossSpawn.sync('npm', ['run', 'stop-server'], {
// If it's taking too long we might as well kill the process and move on,
// mongodb-runer is flaky sometimes and in ci `posttest-ci` script will
// take care of additional clean up anyway
timeout: 30_000,
stdio: 'inherit',
});
} catch (e) {
debug('Failed to stop MongoDB Server', e);
}
try {
fs.rmdirSync('.mongodb', { recursive: true });
} catch (e) {
debug('Failed to clean up server data', e);
}
}
}

import type Mocha from 'mocha';
import path from 'path';
import os from 'os';
Expand Down Expand Up @@ -42,20 +42,16 @@ let j = 0;
// For the html
//let k = 0;

type CompassOptions = {
userDataDir: string;
};

export class Compass {
browser: CompassBrowser;
isFirstRun: boolean;
renderLogs: any[]; // TODO
logs: LogEntry[];
logPath?: string;
userDataDir: string;

constructor(browser: CompassBrowser, options: CompassOptions) {
constructor(browser: CompassBrowser, { isFirstRun = false } = {}) {
this.browser = browser;
this.userDataDir = options.userDataDir;
this.isFirstRun = isFirstRun;
this.logs = [];
this.renderLogs = [];

Expand Down Expand Up @@ -90,7 +86,15 @@ export class Compass {
// first arg is usually == text, but not always
const args = [];
for (const arg of message.args()) {
args.push(await arg.jsonValue());
let value;
try {
value = await arg.jsonValue();
} catch (err) {
// there are still some edge cases we can't easily convert into text
console.error('could not convert', arg);
value = '¯\\_(ツ)_/¯';
}
args.push(value);
}

// uncomment to see browser logs
Expand Down Expand Up @@ -190,16 +194,6 @@ export class Compass {
debug(`Writing Compass application log to ${compassLogPath}`);
await fs.writeFile(compassLogPath, compassLog.raw);
this.logs = compassLog.structured;

debug('Removing user data');
try {
await fs.rmdir(this.userDataDir, { recursive: true });
} catch (e) {
debug(
`Failed to remove temporary user data directory at ${this.userDataDir}:`
);
debug(e);
}
}

async capturePage(
Expand All @@ -215,23 +209,63 @@ export class Compass {
}
}

async function startCompass(
testPackagedApp = ['1', 'true'].includes(process.env.TEST_PACKAGED_APP ?? ''),
opts = {}
): Promise<Compass> {
const nowFormatted = formattedDate();
interface StartCompassOptions {
firstRun?: boolean;
}

const userDataDir = path.join(
os.tmpdir(),
`user-data-dir-${Date.now().toString(32)}-${++i}`
let defaultUserDataDir: string | undefined;

export function removeUserDataDir(): void {
if (!defaultUserDataDir) {
return;
}
debug('Removing user data');
try {
// this is sync so we can use it in cleanup() in index.ts
rmdirSync(defaultUserDataDir, { recursive: true });
} catch (e) {
debug(
`Failed to remove temporary user data directory at ${defaultUserDataDir}:`
);
debug(e);
}
}

async function startCompass(opts: StartCompassOptions = {}): Promise<Compass> {
const testPackagedApp = ['1', 'true'].includes(
process.env.TEST_PACKAGED_APP ?? ''
);

const nowFormatted = formattedDate();

const isFirstRun = opts.firstRun || !defaultUserDataDir;

// If this is not the first run, but we want it to be, delete the user data
// dir so it will be recreated below.
if (defaultUserDataDir && opts.firstRun) {
removeUserDataDir();
// windows seems to be weird about us deleting and recreating this dir, so
// just make a new one for next time
defaultUserDataDir = undefined;
}

// Calculate the userDataDir once so it will be the same between runs. That
// way we can test first run vs second run experience.
if (!defaultUserDataDir) {
defaultUserDataDir = path.join(
os.tmpdir(),
`user-data-dir-${Date.now().toString(32)}-${++i}`
);
}
const chromedriverLogPath = path.join(
LOG_PATH,
`chromedriver.${nowFormatted}.log`
);
const webdriverLogPath = path.join(LOG_PATH, 'webdriver');

await fs.mkdir(userDataDir, { recursive: true });
// Ensure that the user data dir exists
await fs.mkdir(defaultUserDataDir, { recursive: true });

// Chromedriver will fail if log path doesn't exist, webdriver doesn't care,
// for consistency let's mkdir for both of them just in case
await fs.mkdir(path.dirname(chromedriverLogPath), { recursive: true });
Expand All @@ -252,7 +286,7 @@ async function startCompass(
// https://peter.sh/experiments/chromium-command-line-switches/
// https://www.electronjs.org/docs/latest/api/command-line-switches
chromeArgs.push(
`--user-data-dir=${userDataDir}`,
`--user-data-dir=${defaultUserDataDir}`,
// Chromecast feature that is enabled by default in some chrome versions
// and breaks the app on Ubuntu
'--media-router=0',
Expand Down Expand Up @@ -323,7 +357,7 @@ async function startCompass(
// @ts-expect-error
const browser = await remote(options);

const compass = new Compass(browser, { userDataDir });
const compass = new Compass(browser, { isFirstRun });

await compass.recordLogs();

Expand Down Expand Up @@ -514,14 +548,20 @@ function augmentError(error: Error, stack: string) {
error.stack = `${error.stack ?? ''}\nvia ${strippedLines.join('\n')}`;
}

export async function beforeTests(): Promise<Compass> {
const compass = await startCompass();
export async function beforeTests(
opts?: StartCompassOptions
): Promise<Compass> {
const compass = await startCompass(opts);

const { browser } = compass;

await browser.waitForConnectionScreen();
await browser.closeTourModal();
await browser.closePrivacySettingsModal();
if (process.env.SHOW_TOUR) {
await browser.closeTourModal();
}
if (compass.isFirstRun) {
await browser.closePrivacySettingsModal();
}

return compass;
}
Expand Down
16 changes: 15 additions & 1 deletion packages/compass-e2e-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
compileCompassAssets,
buildCompass,
LOG_PATH,
removeUserDataDir,
} from './helpers/compass';
import { createUnlockedKeychain } from './helpers/keychain';
import ResultLogger from './helpers/result-logger';
Expand All @@ -22,6 +23,8 @@ const keychain = createUnlockedKeychain();
// We can't import mongodb here yet because native modules will be recompiled
let metricsClient: MongoClient;

const FIRST_TEST = 'tests/time-to-first-query.test.ts';

async function setup() {
await keychain.activate();

Expand All @@ -48,6 +51,8 @@ async function setup() {
}

function cleanup() {
removeUserDataDir();

keychain.reset();

const disableStartStop = process.argv.includes('--disable-start-stop');
Expand Down Expand Up @@ -112,10 +117,18 @@ async function main() {
}
}

const tests = await promisify(glob)('tests/**/*.{test,spec}.ts', {
const rawTests = await promisify(glob)('tests/**/*.{test,spec}.ts', {
cwd: __dirname,
});

// The only test file that's interested in the first-run experience (at the
// time of writing) is time-to-first-query.ts and that happens to be
// alphabetically right at the end. Which is fine, but the first test to run
// will also get the slow first run experience for no good reason unless it is
// the time-to-first-query.ts test.
// So yeah.. this is a bit of a micro optimisation.
const tests = [FIRST_TEST, ...rawTests.filter((t) => t !== FIRST_TEST)];

const bail = process.argv.includes('--bail');

const mocha = new Mocha({
Expand Down Expand Up @@ -203,6 +216,7 @@ async function run() {
if (metricsClient) {
await metricsClient.close();
}

cleanup();
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/compass-e2e-tests/tests/logging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Logging and Telemetry integration', function () {

before(async function () {
telemetry = await startTelemetryServer();
const compass = await beforeTests();
const compass = await beforeTests({ firstRun: true });
const { browser } = compass;
try {
await browser.connectWithConnectionString(
Expand Down
96 changes: 85 additions & 11 deletions packages/compass-e2e-tests/tests/time-to-first-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,56 @@ import type { Compass } from '../helpers/compass';
import * as Selectors from '../helpers/selectors';

describe('Time to first query', function () {
let compass: Compass;
let compass: Compass | undefined;

it('can open compass, connect to a database and run a query on a collection', async function () {
afterEach(async function () {
// cleanup outside of the test so that the time it takes to run does not
// get added to the time it took to run the first query
delete process.env.SHOW_TOUR;
if (compass) {
// even though this is after (not afterEach) currentTest points to the last test
await afterTest(compass, this.currentTest);
await afterTests(compass);
compass = undefined;
}
});

it('can open compass, connect to a database and run a query on a collection (first run)', async function () {
// start compass inside the test so that the time is measured together
compass = await beforeTests({ firstRun: true });

const { browser } = compass;

await browser.connectWithConnectionString('mongodb://localhost:27018/test');

await browser.navigateToCollectionTab('test', 'numbers', 'Documents');

// search for the document with id == 42 and wait for just one result to appear
const aceCommentElement = await browser.$(
'#query-bar-option-input-filter .ace_scroller'
);
await aceCommentElement.click();

await browser.keys('{ i: 42 }');
const filterButtonElement = await browser.$(
Selectors.queryBarApplyFilterButton('Documents')
);
await filterButtonElement.click();
await browser.waitUntil(async () => {
// we start off with 20 results (assuming no filter) and we expect to
// have just one once the filter finishes
const result = await browser.$$('.document-list .document');
return result.length === 1;
});

const documentElementValue = await browser.$(
'.document-list .document .element-value-is-int32'
);
const text = await documentElementValue.getText();
expect(text).to.equal('42');
});

it('can open compass, connect to a database and run a query on a collection (second run onwards)', async function () {
// start compass inside the test so that the time is measured together
compass = await beforeTests();

Expand Down Expand Up @@ -41,14 +88,41 @@ describe('Time to first query', function () {
expect(text).to.equal('42');
});

// eslint-disable-next-line mocha/no-hooks-for-single-case
after(async function () {
// cleanup outside of the test so that the time it takes to run does not
// get added to the time it took to run the first query
if (compass) {
// even though this is after (not afterEach) currentTest points to the last test
await afterTest(compass, this.currentTest);
await afterTests(compass);
}
it('can open compass, connect to a database and run a query on a collection (new version)', async function () {
// force the tour modal which would normally only appear for new versions
process.env.SHOW_TOUR = 'true';

// start compass inside the test so that the time is measured together
compass = await beforeTests();

const { browser } = compass;

await browser.connectWithConnectionString('mongodb://localhost:27018/test');

await browser.navigateToCollectionTab('test', 'numbers', 'Documents');

// search for the document with id == 42 and wait for just one result to appear
const aceCommentElement = await browser.$(
'#query-bar-option-input-filter .ace_scroller'
);
await aceCommentElement.click();

await browser.keys('{ i: 42 }');
const filterButtonElement = await browser.$(
Selectors.queryBarApplyFilterButton('Documents')
);
await filterButtonElement.click();
await browser.waitUntil(async () => {
// we start off with 20 results (assuming no filter) and we expect to
// have just one once the filter finishes
const result = await browser.$$('.document-list .document');
return result.length === 1;
});

const documentElementValue = await browser.$(
'.document-list .document .element-value-is-int32'
);
const text = await documentElementValue.getText();
expect(text).to.equal('42');
});
});
7 changes: 4 additions & 3 deletions packages/compass/src/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ ipc.once('app:launched', function() {

const { log, mongoLogId, debug, track } =
require('@mongodb-js/compass-logging').createLoggerAndTelemetry('COMPASS-APP');

/**
* The top-level application singleton that brings everything together!
*/
Expand Down Expand Up @@ -309,8 +309,9 @@ var Application = View.extend({
var save = false;
if (
semver.lt(oldVersion, currentVersion) ||
// So we can test the tour in any e2e environment, not only on prod
process.env.APP_ENV === 'webdriverio'
// this is so we can test the tour modal in E2E tests where the version
// is always the same
process.env.SHOW_TOUR
) {
prefs.showFeatureTour = oldVersion;
save = true;
Expand Down