Skip to content

Commit 2f84708

Browse files
committed
test: Add E2E test for first install
Add E2E test to ensure a window opens upon first install. This test needs to use a production-like build rather than a standard E2E build, so the existing "vault decryptor" job was repurposed to be more generically for E2E tests using a production-like build. A global function `reloadExtension` is added to `stateHooks` for use by this test. This function had to be enabled for production builds because the test uses a production build.
1 parent 04b823b commit 2f84708

File tree

11 files changed

+176
-20
lines changed

11 files changed

+176
-20
lines changed

.github/workflows/e2e-chrome.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ jobs:
7171
matrix-index: ${{ matrix.index }}
7272
matrix-total: ${{ strategy.job-total }}
7373

74-
test-e2e-chrome-vault-decryption:
74+
test-e2e-chrome-dist:
7575
uses: ./.github/workflows/run-e2e.yml
7676
with:
77-
test-suite-name: test-e2e-chrome-vault-decryption
77+
test-suite-name: test-e2e-chrome-dist
7878
build-artifact: build-dist-browserify
79-
test-command: yarn test:e2e:single test/e2e/vault-decryption-chrome.spec.ts --browser chrome
79+
test-command: yarn test:e2e:chrome:dist
8080

8181
test-e2e-chrome-api-specs:
8282
uses: ./.github/workflows/run-e2e.yml
@@ -167,7 +167,7 @@ jobs:
167167
- test-e2e-chrome-multiple-providers
168168
- test-e2e-chrome-rpc
169169
- test-e2e-chrome-flask
170-
- test-e2e-chrome-vault-decryption
170+
- test-e2e-chrome-dist
171171
- test-e2e-chrome-api-specs
172172
- test-e2e-chrome-api-specs-multichain
173173
runs-on: ubuntu-latest

.github/workflows/e2e-firefox.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,18 @@ jobs:
3636
matrix-index: ${{ matrix.index }}
3737
matrix-total: ${{ strategy.job-total }}
3838

39+
test-e2e-firefox-dist:
40+
uses: ./.github/workflows/run-e2e.yml
41+
with:
42+
test-suite-name: test-e2e-firefox-dist
43+
build-artifact: build-dist-mv2-browserify
44+
test-command: yarn test:e2e:firefox:dist
45+
3946
test-e2e-firefox-report:
4047
needs:
4148
- test-e2e-firefox-browserify
4249
- test-e2e-firefox-flask
50+
- test-e2e-firefox-dist
4351
runs-on: ubuntu-latest
4452
if: ${{ !cancelled() }}
4553
env:

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ jobs:
251251
e2e-firefox:
252252
needs:
253253
- needs-e2e
254+
- build-dist-mv2-browserify
254255
- build-test-mv2-browserify
255256
- build-test-flask-mv2-browserify
256257
if: ${{ needs.needs-e2e.outputs.needs-e2e == 'true' }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ app/.DS_Store
3232
storybook-build/
3333
coverage/
3434
jest-coverage/
35-
dist
35+
/dist
3636
builds*/
3737
builds.zip
3838
development/ts-migration-dashboard/build

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"test:integration": "npx webpack build --config ./development/webpack/webpack.integration.tests.config.ts && jest --config jest.integration.config.js",
5252
"test:integration:coverage": "yarn test:integration --coverage",
5353
"test:e2e:chrome": "SELENIUM_BROWSER=chrome tsx test/e2e/run-all.ts",
54+
"test:e2e:chrome:dist": "SELENIUM_BROWSER=chrome tsx test/e2e/run-all.ts --dist",
5455
"test:e2e:chrome:flask": "SELENIUM_BROWSER=chrome tsx test/e2e/run-all.ts --build-type flask",
5556
"test:e2e:chrome:webpack": "SELENIUM_BROWSER=chrome tsx test/e2e/run-all.ts",
5657
"test:api-specs": "SELENIUM_BROWSER=chrome tsx test/e2e/run-openrpc-api-test-coverage.ts",
@@ -61,6 +62,7 @@
6162
"test:e2e:chrome:rpc": "SELENIUM_BROWSER=chrome tsx test/e2e/run-all.ts --rpc",
6263
"test:e2e:chrome:multi-provider": "MULTIPROVIDER=true SELENIUM_BROWSER=chrome tsx test/e2e/run-all.ts --multi-provider",
6364
"test:e2e:firefox": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts",
65+
"test:e2e:firefox:dist": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts --dist",
6466
"test:e2e:firefox:flask": "SELENIUM_BROWSER=firefox tsx test/e2e/run-all.ts --build-type flask",
6567
"test:e2e:single": "node test/e2e/run-e2e-test.js",
6668
"ganache:start": "./development/run-ganache.sh",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import assert from 'assert/strict';
2+
import { Browser } from 'selenium-webdriver';
3+
import { withFixtures } from '../helpers';
4+
import { errorMessages } from '../webdriver/driver';
5+
import StartOnboardingPage from '../page-objects/pages/onboarding/start-onboarding-page';
6+
import { hasProperty, isObject } from '@metamask/utils';
7+
8+
describe('First install', function () {
9+
it('opens new window upon install, but not on subsequent reloads', async function () {
10+
await withFixtures(
11+
{
12+
disableServerMochaToBackground: true,
13+
},
14+
async ({ driver }) => {
15+
// Wait for MetaMask to automatically open a new tab
16+
await driver.waitUntilXWindowHandles(2);
17+
18+
let windowHandles = await driver.getAllWindowHandles();
19+
20+
// Switch to new tab and verify it's the start onboarding page
21+
await driver.driver.switchTo().window(windowHandles[1]);
22+
const startOnboardingPage = new StartOnboardingPage(driver);
23+
await startOnboardingPage.check_pageIsLoaded();
24+
25+
await driver.executeScript('window.stateHooks.reloadExtension()');
26+
27+
// Wait for extension to reload, signified by the onboarding tab closing
28+
await driver.waitUntilXWindowHandles(1);
29+
30+
// Test to see if it re-opens
31+
try {
32+
await driver.waitUntilXWindowHandles(2);
33+
} catch (error) {
34+
if (
35+
isObject(error) &&
36+
hasProperty(error, 'message') &&
37+
typeof error.message === 'string' &&
38+
error.message.startsWith(
39+
errorMessages.waitUntilXWindowHandlesTimeout,
40+
)
41+
) {
42+
// Ignore timeout error, it's expected here in the success case
43+
console.log('Onboarding tab not opened');
44+
return;
45+
} else {
46+
throw error;
47+
}
48+
}
49+
throw new Error('Onboarding tab opened unexpectedly');
50+
},
51+
);
52+
});
53+
});

test/e2e/vault-decryption-chrome.spec.ts renamed to test/e2e/dist/vault-decryption-chrome.spec.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import os from 'os';
22
import path from 'path';
33
import fs from 'fs-extra';
44
import level from 'level';
5-
import { Driver } from './webdriver/driver';
6-
import { WALLET_PASSWORD, WINDOW_TITLES, withFixtures } from './helpers';
7-
import HeaderNavbar from './page-objects/pages/header-navbar';
8-
import HomePage from './page-objects/pages/home/homepage';
9-
import PrivacySettings from './page-objects/pages/settings/privacy-settings';
10-
import SettingsPage from './page-objects/pages/settings/settings-page';
11-
import VaultDecryptorPage from './page-objects/pages/vault-decryptor-page';
12-
import { completeCreateNewWalletOnboardingFlowWithCustomSettings } from './page-objects/flows/onboarding.flow';
5+
import { Driver } from '../webdriver/driver';
6+
import { WALLET_PASSWORD, WINDOW_TITLES, withFixtures } from '../helpers';
7+
import HeaderNavbar from '../page-objects/pages/header-navbar';
8+
import HomePage from '../page-objects/pages/home/homepage';
9+
import PrivacySettings from '../page-objects/pages/settings/privacy-settings';
10+
import SettingsPage from '../page-objects/pages/settings/settings-page';
11+
import VaultDecryptorPage from '../page-objects/pages/vault-decryptor-page';
12+
import { completeCreateNewWalletOnboardingFlowWithCustomSettings } from '../page-objects/flows/onboarding.flow';
1313

1414
const VAULT_DECRYPTOR_PAGE = 'https://metamask.github.io/vault-decryptor';
1515

@@ -162,6 +162,10 @@ async function closePopoverIfPresent(driver: Driver) {
162162

163163
describe('Vault Decryptor Page', function () {
164164
it('is able to decrypt the vault uploading the log file in the vault-decryptor webapp', async function () {
165+
if (process.env.SELENIUM_BROWSER !== 'chrome') {
166+
// TODO: Get this working on Firefox
167+
this.skip();
168+
}
165169
await withFixtures(
166170
{
167171
disableServerMochaToBackground: true,
@@ -222,6 +226,10 @@ describe('Vault Decryptor Page', function () {
222226
});
223227

224228
it('is able to decrypt the vault pasting the text in the vault-decryptor webapp', async function () {
229+
if (process.env.SELENIUM_BROWSER !== 'chrome') {
230+
// TODO: Get this working on Firefox
231+
this.skip();
232+
}
225233
await withFixtures(
226234
{
227235
disableServerMochaToBackground: true,

test/e2e/run-all.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ async function main(): Promise<void> {
9595
description: `run json-rpc specific e2e tests`,
9696
type: 'boolean',
9797
})
98+
.option('dist', {
99+
description: `run e2e tests for production-like builds`,
100+
type: 'boolean',
101+
})
98102
.option('multi-provider', {
99103
description: `run multi injected provider e2e tests`,
100104
type: 'boolean',
@@ -128,6 +132,7 @@ async function main(): Promise<void> {
128132
const {
129133
browser,
130134
debug,
135+
dist,
131136
retries,
132137
rpc,
133138
buildType,
@@ -137,6 +142,7 @@ async function main(): Promise<void> {
137142
} = argv as {
138143
browser?: 'chrome' | 'firefox';
139144
debug?: boolean;
145+
dist?: boolean;
140146
retries?: number;
141147
rpc?: boolean;
142148
buildType?: string;
@@ -161,6 +167,9 @@ async function main(): Promise<void> {
161167
...(await getTestPathsForTestDir(path.join(__dirname, 'flask'))),
162168
...featureTestsOnMain,
163169
];
170+
} else if (dist) {
171+
const testDir = path.join(__dirname, 'dist');
172+
testPaths = await getTestPathsForTestDir(testDir);
164173
} else if (rpc) {
165174
const testDir = path.join(__dirname, 'json-rpc');
166175
testPaths = await getTestPathsForTestDir(testDir);

test/e2e/webdriver/driver.js

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
const cssToXPath = require('css-to-xpath');
1313
const { sprintf } = require('sprintf-js');
1414
const lodash = require('lodash');
15+
const { retry } = require('../../../development/lib/retry');
1516
const { quoteXPathText } = require('../../helpers/quoteXPathText');
1617
const { isManifestV3 } = require('../../../shared/modules/mv3.utils');
1718
const { WindowHandles } = require('../background-socket/window-handles');
@@ -24,6 +25,14 @@ const PAGES = {
2425
POPUP: 'popup',
2526
};
2627

28+
/**
29+
* Error messages used by driver methods.
30+
*/
31+
const errorMessages = {
32+
waitUntilXWindowHandlesTimeout:
33+
'waitUntilXWindowHandles timed out polling window handles',
34+
};
35+
2736
/**
2837
* Temporary workaround to patch selenium's element handle API with methods
2938
* that match the playwright API for Elements
@@ -128,17 +137,24 @@ class Driver {
128137
* @param {!ThenableWebDriver} driver - A {@code WebDriver} instance
129138
* @param {string} browser - The type of browser this driver is controlling
130139
* @param {string} extensionUrl
140+
* @param {boolean} e2eBuild - Whether the driver is being used with an E2E build or not.
131141
* @param {number} timeout - Defaults to 10000 milliseconds (10 seconds)
132142
*/
133-
constructor(driver, browser, extensionUrl, timeout = 10 * 1000) {
143+
constructor(
144+
driver,
145+
browser,
146+
extensionUrl,
147+
e2eBuild = true,
148+
timeout = 10 * 1000,
149+
) {
134150
this.driver = driver;
135151
this.browser = browser;
136152
this.extensionUrl = extensionUrl;
137153
this.timeout = timeout;
138154
this.exceptions = [];
139155
this.errors = [];
140156
this.eventProcessingStack = [];
141-
this.windowHandles = new WindowHandles(this.driver);
157+
this.windowHandles = e2eBuild ? new WindowHandles(this.driver) : null;
142158

143159
// The following values are found in
144160
// https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/lib/input.js#L50-L110
@@ -1053,7 +1069,9 @@ class Driver {
10531069
*/
10541070
async switchToWindow(handle) {
10551071
await this.driver.switchTo().window(handle);
1056-
await this.windowHandles.getCurrentWindowProperties(null, handle);
1072+
if (this.windowHandles) {
1073+
await this.windowHandles.getCurrentWindowProperties(null, handle);
1074+
}
10571075
}
10581076

10591077
/**
@@ -1082,7 +1100,10 @@ class Driver {
10821100
* be resolved with an array of window handles.
10831101
*/
10841102
async getAllWindowHandles() {
1085-
return await this.windowHandles.getAllWindowHandles();
1103+
if (this.windowHandles) {
1104+
return await this.windowHandles.getAllWindowHandles();
1105+
}
1106+
return await this.driver.getAllWindowHandles();
10861107
}
10871108

10881109
/**
@@ -1153,7 +1174,7 @@ class Driver {
11531174
}
11541175

11551176
throw new Error(
1156-
`waitUntilXWindowHandles timed out polling window handles. Expected: ${x}, Actual: ${windowHandles.length}`,
1177+
`${errorMessages.waitUntilXWindowHandlesTimeout}. Expected: ${x}, Actual: ${windowHandles.length}`,
11571178
);
11581179
}
11591180

@@ -1193,7 +1214,41 @@ class Driver {
11931214
* @throws {Error} throws an error if no window with the specified title is found
11941215
*/
11951216
async switchToWindowWithTitle(title) {
1196-
return await this.windowHandles.switchToWindowWithProperty('title', title);
1217+
if (this.windowHandles) {
1218+
return await this.windowHandles.switchToWindowWithProperty(
1219+
'title',
1220+
title,
1221+
);
1222+
}
1223+
1224+
let windowHandles = await this.driver.getAllWindowHandles();
1225+
let timeElapsed = 0;
1226+
1227+
while (timeElapsed <= this.timeout) {
1228+
for (const handle of windowHandles) {
1229+
const handleTitle = await retry(
1230+
{
1231+
retries: 8,
1232+
delay: 2500,
1233+
},
1234+
async () => {
1235+
await this.driver.switchTo().window(handle);
1236+
return await this.driver.getTitle();
1237+
},
1238+
);
1239+
1240+
if (handleTitle === title) {
1241+
return handle;
1242+
}
1243+
}
1244+
const delayTime = 1000;
1245+
await this.delay(delayTime);
1246+
timeElapsed += delayTime;
1247+
// refresh the window handles
1248+
windowHandles = await this.driver.getAllWindowHandles();
1249+
}
1250+
1251+
throw new Error(`No window with title: ${title}`);
11971252
}
11981253

11991254
/**
@@ -1217,6 +1272,9 @@ class Driver {
12171272
* @throws {Error} throws an error if no window with the specified URL is found
12181273
*/
12191274
async switchToWindowWithUrl(url) {
1275+
if (!this.windowHandles) {
1276+
throw new Error('This is only supported for E2E test builds');
1277+
}
12201278
return await this.windowHandles.switchToWindowWithProperty(
12211279
'url',
12221280
new URL(url).toString(), // Make sure the URL has a trailing slash
@@ -1233,6 +1291,9 @@ class Driver {
12331291
* @throws {Error} throws an error if no window with the specified URL is found
12341292
*/
12351293
async switchToWindowIfKnown(title) {
1294+
if (!this.windowHandles) {
1295+
throw new Error('This is only supported for E2E test builds');
1296+
}
12361297
return await this.windowHandles.switchToWindowIfKnown(title);
12371298
}
12381299

@@ -1625,4 +1686,4 @@ function sanitizeTestTitle(testTitle) {
16251686
.replace(/^-+|-+$/gu, ''); // Trim leading/trailing dashes
16261687
}
16271688

1628-
module.exports = { Driver, PAGES };
1689+
module.exports = { Driver, PAGES, errorMessages };

types/global.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ type StateHooks = {
285285
onInstalledListener?: Promise<{
286286
reason: chrome.runtime.InstalledDetails;
287287
}>;
288+
/**
289+
* Reload the extension. This is used to trigger extension reload from a page context by E2E
290+
* tests.
291+
*/
292+
reloadExtension?: () => void;
288293
};
289294

290295
export declare global {

0 commit comments

Comments
 (0)