diff --git a/.evergreen/functions.yml b/.evergreen/functions.yml index e1fecc79b12..f1dda68ba1d 100644 --- a/.evergreen/functions.yml +++ b/.evergreen/functions.yml @@ -793,8 +793,8 @@ functions: <<: *compass-env <<: *compass-e2e-secrets DEBUG: ${debug|} - COMPASS_E2E_ATLAS_CLOUD_SANDBOX_USERNAME: ${e2e_tests_compass_web_atlas_username} - COMPASS_E2E_ATLAS_CLOUD_SANDBOX_PASSWORD: ${e2e_tests_compass_web_atlas_password} + COMPASS_E2E_ATLAS_CLOUD_USERNAME: ${e2e_tests_compass_web_atlas_username} + COMPASS_E2E_ATLAS_CLOUD_PASSWORD: ${e2e_tests_compass_web_atlas_password} MCLI_PUBLIC_API_KEY: ${e2e_tests_mcli_public_api_key} MCLI_PRIVATE_API_KEY: ${e2e_tests_mcli_private_api_key} MCLI_ORG_ID: ${e2e_tests_mcli_org_id} @@ -815,7 +815,7 @@ functions: # clusters in CI is both pricey and flakey, so we want to limit the # coverage to reduce those factors (at least for now) npm run --unsafe-perm --workspace compass-e2e-tests test-ci -- -- web \ - --test-atlas-cloud-sandbox \ + --test-atlas-cloud \ --test-filter="atlas-cloud/**/*" test-connectivity: diff --git a/.evergreen/start-atlas-cloud-cluster.sh b/.evergreen/start-atlas-cloud-cluster.sh index c4d59a41c8b..078b9414df2 100644 --- a/.evergreen/start-atlas-cloud-cluster.sh +++ b/.evergreen/start-atlas-cloud-cluster.sh @@ -40,14 +40,14 @@ DOCKER_REGISTRY="${DOCKER_REGISTRY:-docker.io}" # MCLI_ORG_ID Org ID # MCLI_PROJECT_ID Project ID # -# COMPASS_E2E_ATLAS_CLOUD_SANDBOX_USERNAME Cloud user you created -# COMPASS_E2E_ATLAS_CLOUD_SANDBOX_PASSWORD Cloud user password +# COMPASS_E2E_ATLAS_CLOUD_USERNAME Cloud user you created +# COMPASS_E2E_ATLAS_CLOUD_PASSWORD Cloud user password # # - Source the script followed by running the tests to make sure that some # variables exported from this script are available for the test env: # # (ATLAS_CLOUD_TEST_CLUSTER_NAME="TestCluster" source .evergreen/start-atlas-cloud-cluster.sh \ -# && npm run -w compass-e2e-tests test web -- --test-atlas-cloud-sandbox --test-filter="atlas-cloud/**/*") +# && npm run -w compass-e2e-tests test web -- --test-atlas-cloud --test-filter="atlas-cloud/**/*") _ATLAS_CLOUD_TEST_CLUSTER_NAME=${ATLAS_CLOUD_TEST_CLUSTER_NAME:-""} @@ -101,8 +101,8 @@ atlascli dbusers create atlasAdmin \ --password "$ATLAS_TEST_DB_PASSWORD" \ --deleteAfter "$DELETE_AFTER" # so that it's autoremoved if cleaning up failed for some reason -export COMPASS_E2E_ATLAS_CLOUD_SANDBOX_DBUSER_USERNAME="$ATLAS_TEST_DB_USERNAME" -export COMPASS_E2E_ATLAS_CLOUD_SANDBOX_DBUSER_PASSWORD="$ATLAS_TEST_DB_PASSWORD" +export COMPASS_E2E_ATLAS_CLOUD_DBUSER_USERNAME="$ATLAS_TEST_DB_USERNAME" +export COMPASS_E2E_ATLAS_CLOUD_DBUSER_PASSWORD="$ATLAS_TEST_DB_PASSWORD" echo "Creating Atlas deployment \`$ATLAS_CLUSTER_NAME\` to test against..." (atlascli clusters create $ATLAS_CLUSTER_NAME \ @@ -117,7 +117,7 @@ atlascli clusters watch $ATLAS_CLUSTER_NAME echo "Getting connection string for provisioned cluster..." CONNECTION_STRINGS_JSON="$(atlascli clusters connectionStrings describe $ATLAS_CLUSTER_NAME -o json)" -export COMPASS_E2E_ATLAS_CLOUD_SANDBOX_CLOUD_CONFIG=$( +export COMPASS_E2E_ATLAS_CLOUD_ENVIRONMENT=$( if [[ "$MCLI_OPS_MANAGER_URL" =~ "-dev" ]]; then echo "dev" elif [[ "$MCLI_OPS_MANAGER_URL" =~ "-qa" ]]; then @@ -126,7 +126,7 @@ export COMPASS_E2E_ATLAS_CLOUD_SANDBOX_CLOUD_CONFIG=$( echo "prod" fi ) -echo "Cloud config: $COMPASS_E2E_ATLAS_CLOUD_SANDBOX_CLOUD_CONFIG" +echo "Cloud environment: $COMPASS_E2E_ATLAS_CLOUD_ENVIRONMENT" -export COMPASS_E2E_ATLAS_CLOUD_SANDBOX_DEFAULT_CONNECTIONS="{\"$ATLAS_CLUSTER_NAME\": $CONNECTION_STRINGS_JSON}" +export COMPASS_E2E_ATLAS_CLOUD_DEFAULT_CONNECTIONS="{\"$ATLAS_CLUSTER_NAME\": $CONNECTION_STRINGS_JSON}" echo "Cluster connections: $COMPASS_E2E_ATLAS_CLOUD_SANDBOX_DEFAULT_CONNECTIONS" diff --git a/package-lock.json b/package-lock.json index d0f7fd50121..e9df4232a9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50115,6 +50115,7 @@ "glob": "^10.2.5", "globals": "^15.14.0", "hadron-build": "^25.8.25", + "jszip": "^3.10.1", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.21.0", @@ -76604,6 +76605,7 @@ "glob": "^10.2.5", "globals": "^15.14.0", "hadron-build": "^25.8.25", + "jszip": "^3.10.1", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.21.0", diff --git a/packages/compass-e2e-tests/helpers/commands/atlas-cloud/auth.ts b/packages/compass-e2e-tests/helpers/commands/atlas-cloud/auth.ts new file mode 100644 index 00000000000..f8592235264 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/atlas-cloud/auth.ts @@ -0,0 +1,67 @@ +import type { CompassBrowser } from '../../compass-browser'; + +export async function signInToAtlasCloudAccount( + browser: CompassBrowser, + signInPageUrl: string, + atlasCloudPageUrl: string, + username: string, + password: string +) { + await browser.navigateTo(signInPageUrl); + + await browser.waitForLeafygreenEnabled('input[name="username"]'); + await browser.$('input[name="username"]').setValue(username); + + await browser.waitForLeafygreenEnabled('button=Next'); + await browser.$('button=Next').click(); + + await browser.$('input[name="password"]').waitForEnabled(); + await browser.$('input[name="password"]').setValue(password); + + await browser.$('button=Login').waitForEnabled(); + await browser.$('button=Login').click(); + + let authenticated = false; + + // Atlas Cloud will periodically remind user to enable MFA (which we can't + // enable in e2e CI environment), so to account for that, in parallel to + // waiting for auth to finish, we'll wait for the MFA screen to show up and + // skip it if it appears + const [, authenticationPromiseSettled] = await Promise.allSettled([ + (async () => { + const remindMeLaterButton = 'button*=Remind me later'; + + await browser.waitUntil( + async () => { + return ( + authenticated || + (await browser.$(remindMeLaterButton).isDisplayed()) + ); + }, + // Takes awhile for the redirect to land on this reminder page when it + // happens, so no need to bombard the browser with displayed checks + { interval: 2000 } + ); + + if (authenticated) { + return; + } + + await browser.clickVisible(remindMeLaterButton); + })(), + browser.waitUntil( + async () => { + const pageUrl = await browser.getUrl(); + // We don't check the exact project id, just want to make sure we are in + // logged in part of atlas cloud + return (authenticated = pageUrl.startsWith(`${atlasCloudPageUrl}/v2/`)); + }, + // See above + { interval: 2000 } + ), + ]); + + if (authenticationPromiseSettled.status === 'rejected') { + throw authenticationPromiseSettled.reason; + } +} diff --git a/packages/compass-e2e-tests/helpers/commands/connect-form.ts b/packages/compass-e2e-tests/helpers/commands/connect-form.ts index c7f3752d556..b057af328ac 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect-form.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect-form.ts @@ -6,8 +6,7 @@ import type { ConnectFormState } from '../connect-form-state'; import Debug from 'debug'; import { DEFAULT_CONNECTIONS, - isTestingAtlasCloudExternal, - isTestingAtlasCloudSandbox, + isTestingAtlasCloud, } from '../test-runner-context'; import { getConnectionTitle } from '@mongodb-js/connection-info'; const debug = Debug('compass-e2e-tests'); @@ -925,7 +924,7 @@ let screenshotCounter = 0; export async function setupDefaultConnections(browser: CompassBrowser) { // When running tests against Atlas Cloud, connections can't be added or // removed from the UI manually, so we skip setup for default connections - if (isTestingAtlasCloudExternal() || isTestingAtlasCloudSandbox()) { + if (isTestingAtlasCloud()) { return; } diff --git a/packages/compass-e2e-tests/helpers/commands/connect.ts b/packages/compass-e2e-tests/helpers/commands/connect.ts index 13e37896057..4a11219654d 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect.ts @@ -9,8 +9,7 @@ import * as Selectors from '../selectors'; import Debug from 'debug'; import { DEFAULT_CONNECTION_NAMES, - isTestingAtlasCloudExternal, - isTestingAtlasCloudSandbox, + isTestingAtlasCloud, } from '../test-runner-context'; const debug = Debug('compass-e2e-tests'); @@ -52,7 +51,7 @@ export async function connectWithConnectionString( // When testing Atlas Cloud, we can't really create a new connection, so just // assume a connection name was passed (with a fallback to a default one) and // try to use it - if (isTestingAtlasCloudExternal() || isTestingAtlasCloudSandbox()) { + if (isTestingAtlasCloud()) { await browser.connectByName( connectionStringOrName ?? DEFAULT_CONNECTION_NAME_1 ); diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index f5a6da81efb..b23ef695cb2 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -71,3 +71,5 @@ export * from './get-open-modals'; export * from './is-modal-open'; export * from './is-modal-eventually-open'; export * from './wait-for-open-modal'; +export * from './leafygreen'; +export * from './atlas-cloud/auth'; diff --git a/packages/compass-e2e-tests/helpers/commands/leafygreen.ts b/packages/compass-e2e-tests/helpers/commands/leafygreen.ts new file mode 100644 index 00000000000..9ec982f1e9e --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/leafygreen.ts @@ -0,0 +1,22 @@ +import type { ChainablePromiseElement } from 'webdriverio'; +import type { CompassBrowser } from '../compass-browser'; + +export const isLeafygreenEnabled = async ( + browser: CompassBrowser, + el: string | ChainablePromiseElement +) => { + el = typeof el === 'string' ? browser.$(el) : el; + return ( + (await el.getAttribute('aria-disabled')) !== 'true' && + (await el.isEnabled()) + ); +}; + +export const waitForLeafygreenEnabled = async ( + browser: CompassBrowser, + el: string | ChainablePromiseElement +) => { + return await browser.waitUntil(async () => { + return await isLeafygreenEnabled(browser, el); + }); +}; diff --git a/packages/compass-e2e-tests/helpers/commands/sidebar-connection.ts b/packages/compass-e2e-tests/helpers/commands/sidebar-connection.ts index 2187bfb5bfa..7e45573e7bc 100644 --- a/packages/compass-e2e-tests/helpers/commands/sidebar-connection.ts +++ b/packages/compass-e2e-tests/helpers/commands/sidebar-connection.ts @@ -71,7 +71,7 @@ export async function selectConnectionMenuItem( // Hover over an arbitrary other element to ensure that the second hover will // actually be a fresh one. This otherwise breaks if this function is called // twice in a row. - await browser.hover(`*:not(${selector}, ${selector} *)`); + await browser.hover(Selectors.ConnectionsTitle); await browser.hover(selector); return false; diff --git a/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts b/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts index caccb9f9608..f08493861ed 100644 --- a/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts +++ b/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts @@ -1,14 +1,7 @@ -import assert from 'node:assert/strict'; - import crossSpawn from 'cross-spawn'; -import { remote } from 'webdriverio'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; import Debug from 'debug'; -import { - COMPASS_WEB_SANDBOX_RUNNER_PATH, - COMPASS_WEB_WDIO_USER_DATA_PATH, - MONOREPO_ELECTRON_CHROMIUM_VERSION, - ELECTRON_PATH, -} from './test-runner-paths'; import type { ConnectionInfo } from '@mongodb-js/connection-info'; import ConnectionString from 'mongodb-connection-string-url'; @@ -21,6 +14,7 @@ const debug = Debug('compass-e2e-tests:compass-web-sandbox'); process.env.OPEN_BROWSER = 'false'; // tell webpack dev server not to open the default browser process.env.DISABLE_DEVSERVER_OVERLAY = 'true'; process.env.APP_ENV = 'webdriverio'; +process.env.COMPASS_WEB_EXPOSE_INTERNALS = 'true'; const wait = (ms: number) => { return new Promise((resolve) => { @@ -28,11 +22,32 @@ const wait = (ms: number) => { }); }; -export function spawnCompassWebSandbox() { +const waitUntil = async ( + fn: () => boolean | Promise, + signal: AbortSignal, + timeoutMs = 120_000, + timeoutMessage = `Timed out while waiting for`, + intervalMs = 2000 +): Promise => { + let done = false; + const start = Date.now(); + while (!done) { + if (signal.aborted) { + return; + } + if (Date.now() - start >= timeoutMs) { + throw new Error(timeoutMessage); + } + await wait(intervalMs); + done = await fn(); + } +}; + +export function spawnCompassWebSandbox(signal: AbortSignal) { const proc = crossSpawn.spawn( 'npm', ['run', '--unsafe-perm', 'start', '--workspace', '@mongodb-js/compass-web'], - { env: process.env } + { env: process.env, signal } ); proc.stdout.pipe(process.stdout); proc.stderr.pipe(process.stderr); @@ -43,152 +58,67 @@ export async function waitForCompassWebSandboxToBeReady( sandboxUrl: string, signal: AbortSignal ) { - let serverReady = false; - const start = Date.now(); - while (!serverReady) { - if (signal.aborted) { - return; - } - if (Date.now() - start >= 120_000) { - throw new Error( - 'The compass-web sandbox is still not running after 120000ms' - ); - } - // No point in trying to fetch sandbox URL right away, give the spawn script - // some time to run - await wait(2000); - try { - const res = await fetch(sandboxUrl); - serverReady = res.ok; - debug('Web server ready:', serverReady); - } catch (err) { - debug('Failed to connect to dev server:', (err as any).message); - } - } -} - -export async function spawnCompassWebSandboxAndSignInToAtlas( - { - username, - password, - sandboxUrl, - waitforTimeout, - }: { - username: string; - password: string; - sandboxUrl: string; - waitforTimeout: number; - }, - signal: AbortSignal -) { - debug('Starting electron-proxy using webdriver ...'); - - const electronProxyRemote = await remote({ - capabilities: { - browserName: 'chromium', - browserVersion: MONOREPO_ELECTRON_CHROMIUM_VERSION, - 'goog:chromeOptions': { - binary: ELECTRON_PATH, - args: [ - `--user-data-dir=${COMPASS_WEB_WDIO_USER_DATA_PATH}`, - `--app=${COMPASS_WEB_SANDBOX_RUNNER_PATH}`, - ], - }, - 'wdio:enforceWebDriverClassic': true, - }, - waitforTimeout, - }); - - if (signal.aborted) { - return electronProxyRemote; - } - - debug('Signing in to Atlas as %s ...', username); - - const authenticatePromise = fetch(`${sandboxUrl}/authenticate`, { - method: 'POST', - }); - - const authWindowHandler = await electronProxyRemote.waitUntil(async () => { - const handlers = await electronProxyRemote.getWindowHandles(); - // First window is about:blank, second one is the one we triggered above - // with `/authenticate` request - return handlers[1]; - }); - await electronProxyRemote.switchToWindow(authWindowHandler); - - await electronProxyRemote.$('input[name="username"]').waitForEnabled(); - await electronProxyRemote.$('input[name="username"]').setValue(username); - - await electronProxyRemote.$('button=Next').waitForEnabled(); - await electronProxyRemote.$('button=Next').click(); - - await electronProxyRemote.$('input[name="password"]').waitForEnabled(); - await electronProxyRemote.$('input[name="password"]').setValue(password); - - await electronProxyRemote.$('button=Login').waitForEnabled(); - await electronProxyRemote.$('button=Login').click(); - - if (signal.aborted) { - return electronProxyRemote; - } - - debug('Waiting for the auth to finish ...'); - - let authenticatedPromiseSettled = false; - - // Atlas Cloud will periodically remind user to enable MFA (which we can't - // enable in e2e CI environment), so to account for that, in parallel to - // waiting for auth to finish, we'll wait for the MFA screen to show up and - // skip it if it appears - const [, settledRes] = await Promise.allSettled([ - (async () => { - const remindMeLaterButton = 'button*=Remind me later'; - - await electronProxyRemote.waitUntil( - async () => { - return ( - authenticatedPromiseSettled || - (await electronProxyRemote.$(remindMeLaterButton).isDisplayed()) - ); - }, - // Takes awhile for the redirect to land on this reminder page when it - // happens, so no need to bombard the browser with displayed checks - { interval: 2000 } - ); - - if (authenticatedPromiseSettled) { - return; + await waitUntil( + async () => { + try { + const res = await fetch(sandboxUrl); + debug('Web server ready:', res.ok); + return res.ok; + } catch (err) { + debug('Failed to connect to dev server:', (err as any).message); + return false; } - - await electronProxyRemote.$(remindMeLaterButton).click(); - })(), - authenticatePromise.finally(() => { - authenticatedPromiseSettled = true; - }), - ]); - - if (settledRes.status === 'rejected') { - throw settledRes.reason; - } - - const res = settledRes.value; - assert( - res.ok, - `Failed to authenticate in Atlas Cloud: ${res.statusText} (${res.status})` + }, + signal, + 120_000, + 'The compass-web sandbox is still not running after 2 mins' ); +} - const body = await res.json(); - assert( - typeof body === 'object' && body !== null && 'projectId' in body, - 'Expected a project id' +export function buildCompassWebPackage(signal: AbortSignal) { + return promisify(execFile)( + 'npm', + ['run', 'compile', '--workspace', '@mongodb-js/compass-web'], + { env: process.env, signal } ); +} - if (signal.aborted) { - return electronProxyRemote; - } +export function spawnCompassWebStaticServer(signal: AbortSignal) { + const proc = crossSpawn.spawn( + 'npm', + [ + 'run', + '--unsafe-perm', + 'serve-dist', + '--workspace', + '@mongodb-js/compass-web', + ], + { env: process.env, signal } + ); + proc.stdout.pipe(process.stdout); + proc.stderr.pipe(process.stderr); + return proc; +} - return electronProxyRemote; +export async function waitForCompassWebStaticAssetsToBeReady( + assetUrl: string, + signal: AbortSignal +) { + await waitUntil( + async () => { + try { + const res = await fetch(assetUrl, { + method: 'HEAD', + }); + return res.ok; + } catch { + return false; + } + }, + signal, + 120_000, + 'Compass-web assets are still not ready after 2 mins' + ); } export const getAtlasCloudSandboxDefaultConnections = ( diff --git a/packages/compass-e2e-tests/helpers/compass.ts b/packages/compass-e2e-tests/helpers/compass.ts index bfc3919be53..667d9ad27f6 100644 --- a/packages/compass-e2e-tests/helpers/compass.ts +++ b/packages/compass-e2e-tests/helpers/compass.ts @@ -29,7 +29,7 @@ import { isTestingDesktop, context, assertTestingWeb, - isTestingAtlasCloudExternal, + isTestingAtlasCloud, } from './test-runner-context'; import { MONOREPO_ELECTRON_CHROMIUM_VERSION, @@ -45,6 +45,11 @@ import { downloadPath } from './downloads'; import path from 'path'; import { globalFixturesAbortController } from './test-runner-global-fixtures'; import { dialogOpenLocator } from './dialog-open-locator-strategy'; +import { getExtension } from './redirect-extension'; +import { + getCloudUrlsFromContext, + COMPASS_WEB_ENTRYPOINT_HOST, +} from './test-runner-context'; const killAsync = async (pid: number, signal?: string) => { return new Promise((resolve, reject) => { @@ -810,40 +815,57 @@ export async function startBrowser( runCounter++; const { webdriverOptions, wdioOptions } = await processCommonOpts(); - const browserCapabilities: Record> = { - chrome: { - 'goog:chromeOptions': { - prefs: { - 'download.default_directory': downloadPath, - }, + const browserName = context.browserName as 'chrome' | 'firefox'; + const redirectExtension = isTestingAtlasCloud(context) + ? await (async () => { + return getExtension( + COMPASS_WEB_ENTRYPOINT_HOST, + `${context.sandboxUrl}/compass-web.mjs` + ); + })() + : null; + + const browserCapabilities: WebdriverIO.Capabilities = { + 'goog:chromeOptions': { + prefs: { + 'download.default_directory': downloadPath, }, + args: isTestingAtlasCloud(context) + ? [ + // We're going to be hitting localhost from remote domain, LNA needs + // to be disabled + '--disable-features=LocalNetworkAccessChecks', + // Allow to load extensions from cli and don't check when remote + // websites are accessing localhost + '--disable-features=DisableLoadExtensionCommandLineSwitch', + `--load-extension=${redirectExtension!.extensionPath}`, + ] + : [], }, - firefox: { - 'moz:firefoxOptions': { - prefs: { - 'browser.download.dir': downloadPath, - 'browser.download.folderList': 2, - 'browser.download.manager.showWhenStarting': false, - 'browser.helperApps.neverAsk.saveToDisk': '*/*', - // Hide the download (progress) panel - 'browser.download.alwaysOpenPanel': false, - }, + 'moz:firefoxOptions': { + prefs: { + 'browser.download.dir': downloadPath, + 'browser.download.folderList': 2, + 'browser.download.manager.showWhenStarting': false, + 'browser.helperApps.neverAsk.saveToDisk': '*/*', + // Hide the download (progress) panel + 'browser.download.alwaysOpenPanel': false, + + ...(isTestingAtlasCloud(context) && { + // Need to disable LNA (see above) + 'network.lna.skip-domains': '*.mongodb.com', + }), }, }, - } as const; + }; - // webdriverio removed RemoteOptions. It is now - // Capabilities.WebdriverIOConfig, but Capabilities is not exported - const options = { + const options: WebdriverIO.RemoteConfig = { capabilities: { - browserName: context.browserName, + browserName, ...(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, + ...browserCapabilities, }, ...webdriverOptions, ...wdioOptions, @@ -852,68 +874,45 @@ export async function startBrowser( debug('Starting browser via webdriverio with the following configuration:'); debug(JSON.stringify(options, null, 2)); - const browser: CompassBrowser = (await remote(options)) as CompassBrowser; - - if (isTestingAtlasCloudExternal(context)) { - const { - atlasCloudExternalCookiesFile, - atlasCloudExternalUrl, - atlasCloudExternalProjectId, - } = context; - - // To be able to use `setCookies` method, we need to first open any page on - // the same domain as the cookies we are going to set - // https://webdriver.io/docs/api/browser/setCookies/ - await browser.navigateTo(`${atlasCloudExternalUrl}/404`); - - type StoredAtlasCloudCookies = { - name: string; - value: string; - domain: string; - path: string; - secure: boolean; - httpOnly: boolean; - expirationDate: number; - }[]; - - const cookies: StoredAtlasCloudCookies = JSON.parse( - await fs.readFile(atlasCloudExternalCookiesFile, 'utf8') - ); + const browser = (await remote(options)) as CompassBrowser; - await browser.setCookies( - cookies - .filter((cookie) => { - // These are the relevant cookies for auth: - // https://github.com/10gen/mms/blob/6d27992a6ab9ab31471c8bcdaa4e347aa39f4013/server/src/features/com/xgen/svc/cukes/helpers/Client.java#L122-L130 - return ( - cookie.name.includes('mmsa-') || - cookie.name.includes('mdb-sat') || - cookie.name.includes('mdb-srt') - ); - }) - .map((cookie) => ({ - name: cookie.name, - value: cookie.value, - domain: cookie.domain, - path: cookie.path, - secure: cookie.secure, - httpOnly: cookie.httpOnly, - })) + const compass = new Compass(name, browser, { + mode: 'web', + writeCoverage: false, + needsCloseWelcomeModal: false, + }); + + if (isTestingAtlasCloud(context)) { + // In firefox extension needs to be loaded via special Gecko command and + // should be provided as a base64 string with compressed extension + if (browserName === 'firefox') { + await browser.installAddOn(redirectExtension!.extension, true); + } + + const urls = getCloudUrlsFromContext(context); + + await browser.signInToAtlasCloudAccount( + urls.accountUrl, + urls.cloudUrl, + context.atlasCloudUsername, + context.atlasCloudPassword ); + // Disable temporary marketing modal before proceeding + await browser.execute(() => { + globalThis.localStorage.setItem( + 'mdb.dataExplorer.hasShownNewDEModal', + 'true' + ); + }); + await browser.navigateTo( - `${atlasCloudExternalUrl}/v2/${atlasCloudExternalProjectId}#/explorer` + `${urls.cloudUrl}/v2/${context.atlasCloudProjectId}#/explorer` ); } else { await browser.navigateTo(context.sandboxUrl); } - const compass = new Compass(name, browser, { - mode: 'web', - writeCoverage: false, - needsCloseWelcomeModal: false, - }); - return compass; } diff --git a/packages/compass-e2e-tests/helpers/redirect-extension.ts b/packages/compass-e2e-tests/helpers/redirect-extension.ts new file mode 100644 index 00000000000..fccabca902c --- /dev/null +++ b/packages/compass-e2e-tests/helpers/redirect-extension.ts @@ -0,0 +1,95 @@ +import path from 'path'; +import fs from 'fs'; +import JSZip from 'jszip'; +import Debug from 'debug'; +import { tmpdir } from 'os'; + +const debug = Debug('compass-e2e-tests:redirect-extension'); + +const redirectExtensionDir = fs.mkdtempSync( + path.join(tmpdir(), 'redirect-extension-') +); + +function buildManifest() { + return { + manifest_version: 3, + name: 'Redirect compass-web entrypoint', + description: 'Redirect fetching compass-web assets to another server', + version: '1.0', + permissions: ['declarativeNetRequestWithHostAccess'], + host_permissions: ['*://*/*'], + declarative_net_request: { + rule_resources: [ + { + id: 'ruleset', + enabled: true, + path: 'redirect-rules.json', + }, + ], + }, + }; +} + +function buildRedirectRules( + fromEntrypointHost: string, + toEntrypointUrl: string +) { + return [ + { + id: 1, + condition: { + regexFilter: `^https:\\/\\/${fromEntrypointHost}\\/.+?index\\.mjs`, + resourceTypes: ['script'], + }, + action: { + type: 'redirect', + redirect: { + regexSubstitution: toEntrypointUrl, + }, + }, + }, + ]; +} + +const compressedExtensionMap: Map> = new Map(); + +/** + * Bootstraps a web extension in a temp folder that will redirect compass-web + * entrypoint from remote resource to some other url as provided to the method. + */ +async function getExtension( + fromEntrypointHost: string, + toEntrypointUrl: string +): Promise<{ extensionPath: string; extension: string }> { + const key = fromEntrypointHost + toEntrypointUrl; + const maybePromise = compressedExtensionMap.get(key); + const compressedExtension = + maybePromise ?? + (async () => { + debug('Bootstrapping extension at %s', redirectExtensionDir); + await fs.promises.writeFile( + path.join(redirectExtensionDir, 'manifest.json'), + JSON.stringify(buildManifest()) + ); + await fs.promises.writeFile( + path.join(redirectExtensionDir, 'redirect-rules.json'), + JSON.stringify(buildRedirectRules(fromEntrypointHost, toEntrypointUrl)) + ); + debug('Compressing extension into a zip'); + const zip = new JSZip(); + for (const file of ['manifest.json', 'redirect-rules.json']) { + zip.file( + file, + fs.createReadStream(path.join(redirectExtensionDir, file)) + ); + } + return zip.generateAsync({ type: 'base64' }); + })(); + compressedExtensionMap.set(key, compressedExtension); + return { + extensionPath: redirectExtensionDir, + extension: await compressedExtension, + }; +} + +export { getExtension }; diff --git a/packages/compass-e2e-tests/helpers/test-runner-context.ts b/packages/compass-e2e-tests/helpers/test-runner-context.ts index 18ad85d3ede..4ab1ae5aca4 100644 --- a/packages/compass-e2e-tests/helpers/test-runner-context.ts +++ b/packages/compass-e2e-tests/helpers/test-runner-context.ts @@ -7,122 +7,106 @@ import yargs from 'yargs'; import type { Argv, CamelCase } from 'yargs'; import { hideBin } from 'yargs/helpers'; import Debug from 'debug'; -import fs from 'fs'; import { getAtlasCloudSandboxDefaultConnections } from './compass-web-sandbox'; const debug = Debug('compass-e2e-tests:context'); function buildCommonArgs(yargs: Argv) { - return yargs - .option('disable-start-stop', { - type: 'boolean', - description: - 'Disables automatically starting (and stopping) default local test mongodb servers and compass-web sandbox', - }) - .option('test-groups', { - type: 'number', - description: - 'Run tests in batches. Sets the total number of test groups to have', - default: 1, - }) - .option('test-group', { - type: 'number', - description: - 'Run tests in batches. Sets the current test group from the total number', - default: 1, - }) - .option('test-filter', { - type: 'string', - description: 'Filter the spec files picked up for testing', - default: '*', - }) - .option('webdriver-waitfor-timeout', { - type: 'number', - description: 'Set a custom default webdriver waitFor timeout', - default: 1000 * 60 * 2, // 2min, webdriver default is 3s - }) - .option('webdriver-waitfor-interval', { - type: 'number', - description: 'Set a custom default webdriver waitFor interval', - default: 100, // webdriver default is 500ms - }) - .option('mocha-timeout', { - type: 'number', - description: 'Set a custom default mocha timeout', - // 4min, kinda arbitrary, but longer than webdriver-waitfor-timeout so the - // test can fail before Mocha times out - default: 1000 * 60 * 4, - }) - .option('mocha-bail', { - type: 'boolean', - description: 'Bail on the first failing test instead of continuing', - }) - .option('hadron-distribution', { - type: 'string', - description: - 'Configure hadron distribution that will be used when packaging compass for tests (has no effect when testing packaged app)', - default: 'compass', - }) - .option('disable-clipboard-usage', { - type: 'boolean', - description: 'Disable tests that are relying on clipboard', - default: false, - }); -} - -function buildDesktopArgs(yargs: Argv) { return ( yargs - .option('test-packaged-app', { - type: 'boolean', - description: - 'Test a packaged binary instead of running compiled assets directly with electron binary', - default: false, - }) // Skip this step if you are running tests consecutively and don't need to - // rebuild modules all the time. Also no need to ever recompile when testing - // compass-web. + // rebuild modules all the time. Also no need to ever recompile when + // testing compass-web. .option('compile', { type: 'boolean', description: 'When not testing a packaged app, re-compile assets before running tests', default: true, }) - .option('native-modules', { + .option('disable-start-stop', { type: 'boolean', - describe: - 'When not testing a packaaged app, re-compile native modules before running tests', - default: true, + description: + 'Disables automatically starting (and stopping) default local test mongodb servers and compass-web sandbox', + }) + .option('test-groups', { + type: 'number', + description: + 'Run tests in batches. Sets the total number of test groups to have', + default: 1, + }) + .option('test-group', { + type: 'number', + description: + 'Run tests in batches. Sets the current test group from the total number', + default: 1, + }) + .option('test-filter', { + type: 'string', + description: 'Filter the spec files picked up for testing', + default: '*', + }) + .option('webdriver-waitfor-timeout', { + type: 'number', + description: 'Set a custom default webdriver waitFor timeout', + default: 1000 * 60 * 2, // 2min, webdriver default is 3s + }) + .option('webdriver-waitfor-interval', { + type: 'number', + description: 'Set a custom default webdriver waitFor interval', + default: 100, // webdriver default is 500ms + }) + .option('mocha-timeout', { + type: 'number', + description: 'Set a custom default mocha timeout', + // 4min, kinda arbitrary, but longer than webdriver-waitfor-timeout so the + // test can fail before Mocha times out + default: 1000 * 60 * 4, + }) + .option('mocha-bail', { + type: 'boolean', + description: 'Bail on the first failing test instead of continuing', + }) + .option('hadron-distribution', { + type: 'string', + description: + 'Configure hadron distribution that will be used when packaging compass for tests (has no effect when testing packaged app)', + default: 'compass', + }) + .option('disable-clipboard-usage', { + type: 'boolean', + description: 'Disable tests that are relying on clipboard', + default: false, }) - .epilogue( - 'All command line arguments can be also provided as env vars with `COMPASS_E2E_` prefix:\n\n COMPASS_E2E_TEST_PACKAGED_APP=true compass-e2e-tests desktop' - ) ); } -/** - * Variables used by a special use-case of running e2e tests against a - * cloud(-dev).mongodb.com URL. If you're changing anything related to these, - * make sure that the tests in mms are also updated to account for that - */ -const atlasCloudExternalArgs = [ - 'atlas-cloud-external-url', - 'atlas-cloud-external-project-id', - 'atlas-cloud-external-cookies-file', - 'atlas-cloud-external-default-connections-file', -] as const; - -type AtlasCloudExternalArgs = - | (typeof atlasCloudExternalArgs)[number] - | CamelCase<(typeof atlasCloudExternalArgs)[number]>; +function buildDesktopArgs(yargs: Argv) { + return yargs + .option('test-packaged-app', { + type: 'boolean', + description: + 'Test a packaged binary instead of running compiled assets directly with electron binary', + default: false, + }) + .option('native-modules', { + type: 'boolean', + describe: + 'When not testing a packaaged app, re-compile native modules before running tests', + default: true, + }) + .epilogue( + 'All command line arguments can be also provided as env vars with `COMPASS_E2E_` prefix:\n\n COMPASS_E2E_TEST_PACKAGED_APP=true compass-e2e-tests desktop' + ); +} const atlasCloudSandboxArgs = [ - 'atlas-cloud-sandbox-cloud-config', - 'atlas-cloud-sandbox-username', - 'atlas-cloud-sandbox-password', - 'atlas-cloud-sandbox-dbuser-username', - 'atlas-cloud-sandbox-dbuser-password', - 'atlas-cloud-sandbox-default-connections', + 'atlas-cloud-environment', + 'atlas-cloud-project-id', + 'atlas-cloud-username', + 'atlas-cloud-password', + 'atlas-cloud-dbuser-username', + 'atlas-cloud-dbuser-password', + 'atlas-cloud-default-connections', ] as const; type AtlasCloudSandboxArgs = @@ -153,72 +137,48 @@ function buildWebArgs(yargs: Argv) { description: 'Set compass-web sandbox URL', default: 'http://localhost:7777', }) - .option('test-atlas-cloud-sandbox', { + .option('test-atlas-cloud', { type: 'boolean', description: - 'Run compass-web tests against a sandbox with a singed in Atlas Cloud user (allows to test Atlas-only functionality that is only available for Cloud UI backend)', + 'Run compass-web tests against Atlas Cloud with a singed in Atlas Cloud user (allows to test Atlas-only functionality that is only available for Cloud UI backend)', + }) + .options('atlas-cloud-environment', { + choices: ['dev', 'qa', 'staging', 'prod'] as const, + default: 'qa', + description: 'Atlas Cloud environment to test against', }) - .options('atlas-cloud-sandbox-cloud-config', { - choices: ['local', 'dev', 'qa', 'prod'] as const, - description: 'Atlas Cloud config preset for the sandbox', + .option('atlas-cloud-project-id', { + type: 'string', + description: 'Atlas project to test against', + default: process.env.MCLI_PROJECT_ID, }) - .options('atlas-cloud-sandbox-username', { + .options('atlas-cloud-username', { type: 'string', description: 'Atlas Cloud username. Will be used to sign in to an account before running the tests', }) - .options('atlas-cloud-sandbox-password', { + .options('atlas-cloud-password', { type: 'string', description: 'Atlas Cloud user password. Will be used to sign in to an account before running the tests', }) - .options('atlas-cloud-sandbox-dbuser-username', { + .options('atlas-cloud-dbuser-username', { type: 'string', description: 'Atlas Cloud database username. Will be used to prepolulate cluster with data', }) - .options('atlas-cloud-sandbox-dbuser-password', { + .options('atlas-cloud-dbuser-password', { type: 'string', description: 'Atlas Cloud user database user password. Will be used to prepolulate cluster with data', }) - .options('atlas-cloud-sandbox-default-connections', { + .options('atlas-cloud-default-connections', { type: 'string', description: 'Stringified JSON with connections that are expected to be available in the Atlas project', }) .implies({ - 'test-atlas-cloud-sandbox': atlasCloudSandboxArgs, - }) - .option('test-atlas-cloud-external', { - type: 'boolean', - description: - 'Run compass-web tests against an external Atlas Cloud URL (e.g., https://cloud-dev.mongodb.com)', - }) - .option('atlas-cloud-external-url', { - type: 'string', - description: 'External URL to run the tests against', - }) - .option('atlas-cloud-external-project-id', { - type: 'string', - description: 'Atlas `projectId` value', - }) - .option('atlas-cloud-external-cookies-file', { - type: 'string', - description: - 'File with a JSON array of cookie values that should contain Atlas Cloud auth cookies', - }) - .option('atlas-cloud-external-default-connections-file', { - type: 'string', - description: - 'File with JSON array of connections (following ConnectionInfo schema) that are expected to be available in the Atlas project', - }) - .implies({ - 'test-atlas-cloud-external': atlasCloudExternalArgs, - }) - .conflicts({ - 'test-atlas-cloud-external': 'test-atlas-cloud-sandbox', - 'test-atlas-cloud-sandbox': 'test-atlas-cloud-external', + 'test-atlas-cloud': atlasCloudSandboxArgs, }) .epilogue( 'All command line arguments can be also provided as env vars with `COMPASS_E2E_` prefix:\n\n COMPASS_E2E_TEST_ATLAS_CLOUD_EXTERNAL=true compass-e2e-tests web' @@ -258,6 +218,10 @@ type DesktopParsedArgs = CommonParsedArgs & type WebParsedArgs = CommonParsedArgs & BuilderCallbackParsedArgs; +type AtlasCloudParsedArgs = WebParsedArgs & { + [K in AtlasCloudSandboxArgs]: NonNullable; +}; + if (!testEnv) { throw new Error('Test env was not selected'); } @@ -305,36 +269,30 @@ export function assertTestingWeb(ctx = context): asserts ctx is WebParsedArgs { } } -export function isTestingAtlasCloudExternal( +export function isTestingAtlasCloud( ctx = context -): ctx is WebParsedArgs & { - [K in AtlasCloudExternalArgs]: NonNullable; -} { - return isTestingWeb(ctx) && !!ctx.testAtlasCloudExternal; +): ctx is AtlasCloudParsedArgs { + return isTestingWeb(ctx) && !!ctx.testAtlasCloud; } -export function isTestingAtlasCloudSandbox( +export function assertTestingAtlasCloud( ctx = context -): ctx is WebParsedArgs & { - [K in AtlasCloudSandboxArgs]: NonNullable; -} { - return isTestingWeb(ctx) && !!ctx.testAtlasCloudSandbox; -} - -export function assertTestingAtlasCloudSandbox( - ctx = context -): asserts ctx is WebParsedArgs & { - [K in AtlasCloudSandboxArgs]: NonNullable; -} { - if (!isTestingAtlasCloudSandbox(ctx)) { +): asserts ctx is WebParsedArgs & AtlasCloudParsedArgs { + if (!isTestingAtlasCloud(ctx)) { throw new Error(`Expected tested runtime to be web w/ Atlas Cloud account`); } } const contextForPrinting = Object.fromEntries( - Object.entries(context).map(([k, v]) => { - return [k, /password/i.test(k) ? '' : v]; - }) + Object.entries(context) + .filter(([k]) => { + // Leave only camelCased values to avoid printing duplicates for + // readability + return !k.includes('-'); + }) + .map(([k, v]) => { + return [k, /password/i.test(k) ? '' : v]; + }) ); debug('Running tests with the following arguments:', contextForPrinting); @@ -342,22 +300,18 @@ debug('Running tests with the following arguments:', contextForPrinting); process.env.HADRON_DISTRIBUTION ??= context.hadronDistribution; process.env.COMPASS_WEB_HTTP_PROXY_CLOUD_CONFIG ??= - context.atlasCloudSandboxCloudConfig ?? 'dev'; + context.atlasCloudEnvironment ?? 'dev'; const testServerVersion = process.env.MONGODB_VERSION ?? process.env.MONGODB_RUNNER_VERSION; export const DEFAULT_CONNECTIONS: (ConnectionInfo & { testServer?: Partial; -})[] = isTestingAtlasCloudExternal(context) - ? JSON.parse( - fs.readFileSync(context.atlasCloudExternalDefaultConnectionsFile, 'utf-8') - ) - : isTestingAtlasCloudSandbox(context) +})[] = isTestingAtlasCloud(context) ? getAtlasCloudSandboxDefaultConnections( - context.atlasCloudSandboxDefaultConnections, - context.atlasCloudSandboxDbuserUsername, - context.atlasCloudSandboxDbuserPassword + context.atlasCloudDefaultConnections, + context.atlasCloudDbuserUsername, + context.atlasCloudDbuserPassword ) : [ { @@ -402,3 +356,31 @@ export const DEFAULT_CONNECTIONS_SERVER_INFO: { version: string; enterprise: boolean; }[] = []; + +/** + * Hostname from which compass-web entrypoint will be loaded in Atlas Cloud + */ +export const COMPASS_WEB_ENTRYPOINT_HOST = 'downloads.mongodb.com'; + +const CLOUD_URLS = { + dev: { + accountUrl: 'https://account-dev.mongodb.com', + cloudUrl: 'https://cloud-dev.mongodb.com', + }, + qa: { + accountUrl: 'https://account-qa.mongodb.com', + cloudUrl: 'https://cloud-qa.mongodb.com', + }, + staging: { + accountUrl: 'https://account-stage.mongodb.com', + cloudUrl: 'https://cloud-stage.mongodb.com', + }, + prod: { + accountUrl: 'https://account.mongodb.com', + cloudUrl: 'https://cloud.mongodb.com', + }, +} as const; + +export function getCloudUrlsFromContext(context: AtlasCloudParsedArgs) { + return CLOUD_URLS[context.atlasCloudEnvironment as keyof typeof CLOUD_URLS]; +} diff --git a/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts b/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts index a852dc7674e..623f11cf49a 100644 --- a/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts +++ b/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts @@ -4,8 +4,7 @@ import { context, DEFAULT_CONNECTIONS, DEFAULT_CONNECTIONS_SERVER_INFO, - isTestingAtlasCloudExternal, - isTestingAtlasCloudSandbox, + isTestingAtlasCloud, isTestingDesktop, isTestingWeb, } from './test-runner-context'; @@ -23,9 +22,11 @@ import { } from './compass'; import { getConnectionTitle } from '@mongodb-js/connection-info'; import { + buildCompassWebPackage, spawnCompassWebSandbox, - spawnCompassWebSandboxAndSignInToAtlas, + spawnCompassWebStaticServer, waitForCompassWebSandboxToBeReady, + waitForCompassWebStaticAssetsToBeReady, } from './compass-web-sandbox'; export const globalFixturesAbortController = new AbortController(); @@ -90,37 +91,42 @@ export async function mochaGlobalSetup(this: Mocha.Runner) { throwIfAborted(); } - if (isTestingWeb(context) && !isTestingAtlasCloudExternal(context)) { - debug('Starting Compass Web server ...'); - - if (isTestingAtlasCloudSandbox(context)) { - const compassWeb = await spawnCompassWebSandboxAndSignInToAtlas( - { - username: context.atlasCloudSandboxUsername, - password: context.atlasCloudSandboxPassword, - sandboxUrl: context.sandboxUrl, - waitforTimeout: context.webdriverWaitforTimeout, - }, - globalFixturesAbortController.signal - ); - cleanupFns.push(async () => { - await compassWeb.deleteSession({ shutdownDriver: true }); - }); - } else { - const compassWeb = spawnCompassWebSandbox(); - cleanupFns.push(() => { - if (compassWeb.pid) { - debug(`Killing compass-web [${compassWeb.pid}]`); - kill(compassWeb.pid, 'SIGINT'); - } else { - debug('No pid for compass-web'); - } - }); - await waitForCompassWebSandboxToBeReady( - context.sandboxUrl, - globalFixturesAbortController.signal - ); + if (isTestingAtlasCloud(context)) { + if (context.compile) { + debug('Building compass-web library ...'); + await buildCompassWebPackage(globalFixturesAbortController.signal); } + + debug('Starting static server for the compass-web assets ...'); + const staticServer = spawnCompassWebStaticServer( + globalFixturesAbortController.signal + ); + cleanupFns.push(() => { + if (staticServer.pid) { + debug(`Killing static server [${staticServer.pid}]`); + kill(staticServer.pid, 'SIGINT'); + } + }); + await waitForCompassWebStaticAssetsToBeReady( + `${context.sandboxUrl}/assets-manifest.json`, + globalFixturesAbortController.signal + ); + } else if (isTestingWeb(context)) { + debug('Starting compass-web sandbox ...'); + + const compassWeb = spawnCompassWebSandbox( + globalFixturesAbortController.signal + ); + cleanupFns.push(() => { + if (compassWeb.pid) { + debug(`Killing compass-web [${compassWeb.pid}]`); + kill(compassWeb.pid, 'SIGINT'); + } + }); + await waitForCompassWebSandboxToBeReady( + context.sandboxUrl, + globalFixturesAbortController.signal + ); } } diff --git a/packages/compass-e2e-tests/helpers/test-runner-paths.ts b/packages/compass-e2e-tests/helpers/test-runner-paths.ts index d7dea41e93d..90e728c1948 100644 --- a/packages/compass-e2e-tests/helpers/test-runner-paths.ts +++ b/packages/compass-e2e-tests/helpers/test-runner-paths.ts @@ -3,7 +3,6 @@ import electronPath from 'electron'; import electronPackageJson from 'electron/package.json'; // @ts-expect-error no types for this package import { electronToChromium } from 'electron-to-chromium'; -import os from 'os'; if (typeof electronPath !== 'string') { throw new Error( @@ -37,13 +36,3 @@ export const MONOREPO_ELECTRON_VERSION = electronPackageJson.version; export const MONOREPO_ELECTRON_CHROMIUM_VERSION = electronToChromium( MONOREPO_ELECTRON_VERSION ); - -export const COMPASS_WEB_SANDBOX_RUNNER_PATH = path.resolve( - path.dirname(require.resolve('@mongodb-js/compass-web/package.json')), - 'scripts', - 'electron-proxy.js' -); -export const COMPASS_WEB_WDIO_USER_DATA_PATH = path.resolve( - os.tmpdir(), - `wdio-electron-proxy-${Date.now()}` -); diff --git a/packages/compass-e2e-tests/index.ts b/packages/compass-e2e-tests/index.ts index e961e7940f8..9a296fb574d 100644 --- a/packages/compass-e2e-tests/index.ts +++ b/packages/compass-e2e-tests/index.ts @@ -61,13 +61,20 @@ let runnerPromise: Promise | undefined; async function main() { const e2eTestGroupsAmount = context.testGroups; const e2eTestGroup = context.testGroup; - const e2eTestFilter = context.testFilter; + const e2eTestFilter = Array.isArray(context.testFilter) + ? context.testFilter + : [context.testFilter]; const tests = ( - await glob(`tests/**/${e2eTestFilter}.{test,spec}.ts`, { - cwd: __dirname, - }) + await Promise.all( + e2eTestFilter.map((filter) => { + return glob(`tests/**/${filter}.{test,spec}.ts`, { + cwd: __dirname, + }); + }) + ) ) + .flat() .filter((_value, index, array) => { const testsPerGroup = Math.ceil(array.length / e2eTestGroupsAmount); const minGroupIndex = (e2eTestGroup - 1) * testsPerGroup; diff --git a/packages/compass-e2e-tests/package.json b/packages/compass-e2e-tests/package.json index b03bebd2dfe..4bd42811541 100644 --- a/packages/compass-e2e-tests/package.json +++ b/packages/compass-e2e-tests/package.json @@ -56,6 +56,7 @@ "glob": "^10.2.5", "globals": "^15.14.0", "hadron-build": "^25.8.25", + "jszip": "^3.10.1", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.21.0", diff --git a/packages/compass-e2e-tests/tests/atlas-cloud/collection-ai-query.test.ts b/packages/compass-e2e-tests/tests/atlas-cloud/collection-ai-query.test.ts index 68232676205..4d137cc43fc 100644 --- a/packages/compass-e2e-tests/tests/atlas-cloud/collection-ai-query.test.ts +++ b/packages/compass-e2e-tests/tests/atlas-cloud/collection-ai-query.test.ts @@ -10,7 +10,7 @@ import { import type { Compass } from '../../helpers/compass'; import * as Selectors from '../../helpers/selectors'; import { createNumbersCollection } from '../../helpers/insert-data'; -import { isTestingAtlasCloudSandbox } from '../../helpers/test-runner-context'; +import { isTestingAtlasCloud } from '../../helpers/test-runner-context'; import { switchPipelineMode } from '../../helpers/commands/switch-pipeline-mode'; describe('Collection ai query (with real Cloud backend)', function () { @@ -18,7 +18,7 @@ describe('Collection ai query (with real Cloud backend)', function () { let browser: CompassBrowser; before(function () { - if (!isTestingAtlasCloudSandbox()) { + if (!isTestingAtlasCloud()) { this.skip(); } }); diff --git a/packages/compass-e2e-tests/tests/atlas-cloud/global-writes.test.ts b/packages/compass-e2e-tests/tests/atlas-cloud/global-writes.test.ts index a45d5758761..559bead1e97 100644 --- a/packages/compass-e2e-tests/tests/atlas-cloud/global-writes.test.ts +++ b/packages/compass-e2e-tests/tests/atlas-cloud/global-writes.test.ts @@ -10,7 +10,7 @@ import type { CompassBrowser } from '../../helpers/compass-browser'; import { createGeospatialCollection } from '../../helpers/insert-data'; import { DEFAULT_CONNECTION_NAMES, - isTestingAtlasCloudSandbox, + isTestingAtlasCloud, } from '../../helpers/test-runner-context'; type GeoShardingFormData = { @@ -74,7 +74,7 @@ describe('Global writes', function () { let browser: CompassBrowser; before(function () { - if (!isTestingAtlasCloudSandbox()) { + if (!isTestingAtlasCloud()) { this.skip(); } }); diff --git a/packages/compass-e2e-tests/tests/atlas-cloud/rolling-indexes.test.ts b/packages/compass-e2e-tests/tests/atlas-cloud/rolling-indexes.test.ts index 741410c891d..19f08c9c4b9 100644 --- a/packages/compass-e2e-tests/tests/atlas-cloud/rolling-indexes.test.ts +++ b/packages/compass-e2e-tests/tests/atlas-cloud/rolling-indexes.test.ts @@ -9,7 +9,7 @@ import type { CompassBrowser } from '../../helpers/compass-browser'; import { createNumbersCollection } from '../../helpers/insert-data'; import { DEFAULT_CONNECTION_NAMES, - isTestingAtlasCloudSandbox, + isTestingAtlasCloud, } from '../../helpers/test-runner-context'; describe('Rolling indexes', function () { @@ -17,7 +17,7 @@ describe('Rolling indexes', function () { let browser: CompassBrowser; before(function () { - if (!isTestingAtlasCloudSandbox()) { + if (!isTestingAtlasCloud()) { this.skip(); } }); diff --git a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts index 36c69c90f5a..e3dec0afc34 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -699,22 +699,22 @@ describe('Data Modeling tab', function () { // Select the other collection and see that the new relationship is listed await selectCollectionOnTheDiagram(browser, 'test.testCollection-nested'); - const relationshipItem = drawer.$( + const secondCollectionRelationshipItem = drawer.$( Selectors.DataModelCollectionRelationshipItem(relationshipId) ); - await relationshipItem.waitForDisplayed(); - expect(await relationshipItem.getText()).to.include( + await secondCollectionRelationshipItem.waitForDisplayed(); + expect(await secondCollectionRelationshipItem.getText()).to.include( 'testCollection-flat' ); // Edit the relationship - await relationshipItem + await secondCollectionRelationshipItem .$(Selectors.DataModelCollectionRelationshipItemEdit) .waitForDisplayed(); - await relationshipItem + await secondCollectionRelationshipItem .$(Selectors.DataModelCollectionRelationshipItemEdit) .waitForClickable(); - await relationshipItem + await secondCollectionRelationshipItem .$(Selectors.DataModelCollectionRelationshipItemEdit) .click(); @@ -739,16 +739,19 @@ describe('Data Modeling tab', function () { // Select the first collection again and delete the relationship await selectCollectionOnTheDiagram(browser, 'test.testCollection-flat'); - await relationshipItem.waitForDisplayed(); - await relationshipItem + const firstCollectionRelationshipItem = drawer.$( + Selectors.DataModelCollectionRelationshipItem(relationshipId) + ); + await firstCollectionRelationshipItem.waitForDisplayed(); + await firstCollectionRelationshipItem .$(Selectors.DataModelCollectionRelationshipItemDelete) .waitForClickable(); - await relationshipItem + await firstCollectionRelationshipItem .$(Selectors.DataModelCollectionRelationshipItemDelete) .click(); // Verify that the relationship is removed from the list and the diagram - await relationshipItem.waitForDisplayed({ reverse: true }); + await firstCollectionRelationshipItem.waitForDisplayed({ reverse: true }); await getDiagramEdges(browser, 0); }); diff --git a/packages/compass-web/package.json b/packages/compass-web/package.json index faaaf635e67..4163c96203f 100644 --- a/packages/compass-web/package.json +++ b/packages/compass-web/package.json @@ -64,7 +64,8 @@ "test-ci": "npm run test-cov", "reformat": "npm run eslint . -- --fix && npm run prettier -- --write .", "upload-dist": "node --experimental-strip-types scripts/release/upload-dist.mts", - "upload-entrypoint": "node --experimental-strip-types scripts/release/upload-entrypoint.mts" + "upload-entrypoint": "node --experimental-strip-types scripts/release/upload-entrypoint.mts", + "serve-dist": "node --experimental-strip-types scripts/dist-file-server.mts" }, "peerDependencies": { "react": "^17.0.2", diff --git a/packages/compass-web/scripts/dist-file-server.mts b/packages/compass-web/scripts/dist-file-server.mts new file mode 100644 index 00000000000..a6a677de7ed --- /dev/null +++ b/packages/compass-web/scripts/dist-file-server.mts @@ -0,0 +1,84 @@ +/** + * This is a very simple static file server with CORS enabled for compass-web + * distribution. Can be used for e2e / local dev when compass-web in MMS needs + * to be replaced with the local resource + */ +import http from 'http'; +import fs from 'fs'; +import path from 'path'; + +const distDir = path.resolve(import.meta.dirname, '..', 'dist'); + +const contentTypeMap: Record = { + '.js': 'text/javascript', + '.mjs': 'text/javascript', + '.json': 'application/json', + '.ts': 'text/plain', + '.txt': 'text/plain', +}; + +const corsHeaders = { + 'access-control-allow-headers': '*', + 'access-control-allow-methods': '*', + 'access-control-allow-origin': '*', +}; + +const server = http.createServer(async (req, res) => { + res.on('close', () => { + console.debug( + '[compass-web-dist-file-server] "%s %s HTTP/%s" %s (%s)', + req.method, + req.url, + req.httpVersion, + res.statusCode, + res.statusMessage + ); + }); + + const requestedPath = path.join( + distDir, + new URL(req.url ?? '/', 'http://localhost').pathname + ); + + if (req.method === 'OPTIONS') { + res.writeHead(200, corsHeaders); + res.end(); + return; + } + + if (req.method === 'GET' || req.method === 'HEAD') { + try { + if ( + fs.existsSync(requestedPath) && + (await fs.promises.stat(requestedPath)).isFile() + ) { + res.writeHead(200, { + ...corsHeaders, + 'content-type': + contentTypeMap[path.extname(requestedPath)] ?? + 'application/octet-stream', + }); + if (req.method === 'GET') { + fs.createReadStream(requestedPath).pipe(res); + } else { + res.end(); + } + } else { + res.writeHead(404); + res.end(); + } + } catch (err) { + res.writeHead(500); + res.end((err as Error).stack); + } + + return; + } + + res.writeHead(405); + res.end(); +}); + +server.listen(7777, 'localhost', () => { + console.debug('[compass-web-dist-file-server] listening on localhost:7777'); +}); diff --git a/packages/compass-web/webpack.config.js b/packages/compass-web/webpack.config.js index cd229590fc5..f177a5cf63a 100644 --- a/packages/compass-web/webpack.config.js +++ b/packages/compass-web/webpack.config.js @@ -346,6 +346,17 @@ module.exports = (env, args) => { clean: true, }; + // Add code that exposes some internals of compass-web. Useful for e2e tests / + // local sync + if (process.env.COMPASS_WEB_EXPOSE_INTERNALS === 'true') { + config.entry.index = [ + path.resolve(__dirname, 'sandbox', 'sandbox-process.ts'), + path.resolve(__dirname, 'sandbox', 'sandbox-preferences.ts'), + path.resolve(__dirname, 'sandbox', 'sandbox-logger-and-telemetry.ts'), + config.entry.index, + ]; + } + return merge(config, { module: { rules: [