Skip to content

Commit cbf33a0

Browse files
authored
Start Edge with a clean profile on Android (#2647)
Edge kept its data between tests, because it showed welcome screens when the data was removed. That left state from earlier tests, so Browsertime had to clear the cache and cookies, close restored tabs and leave an empty page behind, and Edge results were still not comparable with Chrome and Samsung Internet, which start with no data. Edge skips the welcome screens with --disable-fre, but when it starts with no data and no URL, as Chromedriver starts it, it shows its new tab page on top of the tab and never paints the measured page. It also asks for the notification permission again. Browsertime now clears the data itself, gives the permission and opens Edge once on a local error page, so Edge starts on a normal tab. This makes each Edge test about 20 seconds longer, and all the Edge code for this is in one place. Assisted-by: Claude Opus 5.5 noreply@anthropic.com
1 parent 5fffc31 commit cbf33a0

7 files changed

Lines changed: 77 additions & 47 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ If you want to set connectivity you need to use something like [gnirehtet](https
136136
$ browsertime --chrome.android.package com.android.chrome https://www.sitespeed.io --video --visualMetrics
137137
</pre>
138138

139-
To test with Edge, use `--android -b edge`. To test with Samsung Internet, use `--android -b samsung`. Edge shows welcome screens and dialogs when its data is removed, so Browsertime keeps its data and clears the cache and cookies before each test instead. Before the first test, open Edge on the phone and accept the welcome screen and the dialogs. Local storage and service workers are kept between tests. Samsung Internet starts with no data for each test, the same as Chrome. If the phone uses a dark theme, Samsung Internet also shows web pages in dark mode. To test the pages as most users see them, turn off the dark theme on the phone, for example with `adb shell cmd uimode night no`.
139+
To test with Edge, use `--android -b edge`. To test with Samsung Internet, use `--android -b samsung`. Both start with no data for each test, the same as Chrome. Edge shows its new tab page on top of the page when it starts with no data, so before each test Browsertime opens Edge once with an error page. That makes each test about 20 seconds longer. If the phone uses a dark theme, Samsung Internet also shows web pages in dark mode. To test the pages as most users see them, turn off the dark theme on the phone, for example with `adb shell cmd uimode night no`.
140140

141141
To check that all Android features work with your phone, run `npm run test:android` in a Browsertime checkout. It tests Chrome and Firefox (use `npm run test:android -- --browser chrome` or `-- --browser firefox` to test only one) and prints which features pass. Add `--rooted` (for example `npm run test:android -- --rooted`) to also test `--android.rooted`, which changes the CPU, GPU and kernel settings on the phone until it reboots.
142142

‎lib/android/edge.js‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { getLogger } from '@sitespeed.io/log';
2+
import { Android } from './index.js';
3+
const log = getLogger('browsertime.android');
4+
const delay = ms => new Promise(res => setTimeout(res, ms));
5+
6+
const EDGE_PACKAGE = 'com.microsoft.emmx';
7+
// The port is not safe, so Edge shows an error page at once and does not use
8+
// the network.
9+
const FIRST_URL = 'http://127.0.0.1:1/';
10+
11+
/**
12+
* Edge on Android needs more than Chromedriver gives it to start with no data.
13+
* Chromedriver clears the data and starts Edge without a URL. Edge then shows
14+
* its new tab page on top of the tab, and does not paint the page that we
15+
* measure in that tab. Clearing the data also removes the notification
16+
* permission, and Edge asks for it on top of the page. Browsertime therefore
17+
* clears the data, gives the permission and opens a page once, so Edge saves a
18+
* normal tab. Chromedriver keeps that data and Edge opens that tab when it
19+
* starts.
20+
*/
21+
export class AndroidEdge {
22+
static isConfigured(browserOptions) {
23+
return browserOptions.android?.package === EDGE_PACKAGE;
24+
}
25+
26+
constructor(options) {
27+
this.android = new Android(options);
28+
}
29+
30+
async startWithCleanProfile() {
31+
log.info('Clear the data of Edge and open it once');
32+
const run = command => this.android._runCommand(command);
33+
await run(`am force-stop ${EDGE_PACKAGE}`);
34+
await run(`pm clear ${EDGE_PACKAGE}`);
35+
await run(`pm grant ${EDGE_PACKAGE} android.permission.POST_NOTIFICATIONS`);
36+
// Edge reads the flags file, with --disable-fre, only as the debug app.
37+
await run(`am set-debug-app --persistent ${EDGE_PACKAGE}`);
38+
await run(
39+
`am start -W -a android.intent.action.VIEW -p ${EDGE_PACKAGE} -d ${FIRST_URL}`
40+
);
41+
// The first start can take more time than am start waits, and Edge can
42+
// show other activities before the tab.
43+
const end = Date.now() + 30_000;
44+
for (;;) {
45+
const window = await this.android.getFocusedWindow();
46+
if (window.includes('ChromeTabbedActivity')) {
47+
break;
48+
}
49+
if (Date.now() > end) {
50+
log.info(
51+
'Edge did not show the tab, the page can be hidden in the test'
52+
);
53+
break;
54+
}
55+
await delay(500);
56+
}
57+
await delay(2000);
58+
// Edge saves its tabs when it goes to the background.
59+
await this.android.pressHomeButton();
60+
await delay(2000);
61+
await run(`am force-stop ${EDGE_PACKAGE}`);
62+
}
63+
}

‎lib/chrome/chromeDevtoolsProtocol.js‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,6 @@ export class ChromeDevtoolsProtocol {
7474
throw error;
7575
}
7676
}
77-
async closeOtherTabs(target) {
78-
const connection = { local: true, port: this.options.devToolsPort };
79-
const tabs = await CDP.List(connection);
80-
// Never close the tab that WebDriver uses.
81-
if (!tabs.some(tab => tab.id === target)) {
82-
return;
83-
}
84-
for (const tab of tabs) {
85-
if (tab.type === 'page' && tab.id !== target) {
86-
await CDP.Close({ ...connection, id: tab.id });
87-
}
88-
}
89-
}
9077

9178
async startTrace() {
9279
this.events = [];

‎lib/chrome/webdriver/builder.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
setupChromiumOptions,
66
writeAndroidCommandLine
77
} from './setupChromiumOptions.js';
8+
import { AndroidEdge } from '../../android/edge.js';
89
import { pick, isEmpty, getProperty } from '../../support/util.js';
910

1011
/**
@@ -81,6 +82,9 @@ export async function configureBuilder(builder, baseDir, options, tmpDir) {
8182

8283
setupChromiumOptions(chromeOptions, chromeConfig, options, baseDir);
8384
await writeAndroidCommandLine(chromeConfig, options);
85+
if (AndroidEdge.isConfigured(chromeConfig)) {
86+
await new AndroidEdge(options).startWithCleanProfile();
87+
}
8488

8589
builder.setChromeOptions(chromeOptions);
8690
}

‎lib/chrome/webdriver/chromium.js‎

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import path from 'node:path';
44
import { logging } from 'selenium-webdriver';
55
import { getLogger } from '@sitespeed.io/log';
66
const log = getLogger('browsertime.chrome');
7-
const delay = ms => new Promise(res => setTimeout(res, ms));
87
const { Type } = logging;
98
import { longTaskMetrics } from '../longTaskMetrics.js';
109
import { parseCPUTrace } from '../parseCpuTrace.js';
@@ -23,10 +22,7 @@ import { ChromeDevtoolsProtocol } from '../chromeDevtoolsProtocol.js';
2322
import { Coverage } from '../coverage.js';
2423
import { NetworkManager } from '../networkManager.js';
2524
import { Android, isAndroidConfigured } from '../../android/index.js';
26-
import {
27-
getAndroidDevtoolsSocket,
28-
keepsAndroidAppData
29-
} from './setupChromiumOptions.js';
25+
import { getAndroidDevtoolsSocket } from './setupChromiumOptions.js';
3026
import { getRenderBlocking } from './traceUtilities.js';
3127
import { loafTimeLine } from './devtools/loaf.js';
3228
import { lcpTimeLine } from './devtools/lcp.js';
@@ -104,10 +100,6 @@ export class Chromium {
104100
? await runner.getDriver().getWindowHandle()
105101
: undefined;
106102
await this.cdpClient.setup(target);
107-
if (keepsAndroidAppData(this.chrome)) {
108-
// The browser keeps its tabs between tests when we keep its data.
109-
await this.cdpClient.closeOtherTabs(target);
110-
}
111103
}
112104

113105
/**
@@ -123,7 +115,7 @@ export class Chromium {
123115
await this.cdpClient.blockUrls(this.options.block);
124116
}
125117

126-
if (this.options.cacheClearRaw || keepsAndroidAppData(this.chrome)) {
118+
if (this.options.cacheClearRaw) {
127119
await this.cdpClient.clearBrowserCache();
128120
await this.cdpClient.clearBrowserCookies();
129121
}
@@ -718,20 +710,7 @@ export class Chromium {
718710
/**
719711
* Before the browser is stopped/closed.
720712
*/
721-
async beforeBrowserStop(runner) {
722-
if (keepsAndroidAppData(this.chrome)) {
723-
// The browser opens the last page again when it starts, and that page
724-
// opens a connection that the next test then uses. The browser saves
725-
// its tabs when it goes to the background, so press home after.
726-
try {
727-
await runner.getDriver().get('data:,');
728-
await delay(2000);
729-
await this.android.pressHomeButton();
730-
} catch (error) {
731-
log.info('Could not open about:blank before the browser stops', error);
732-
}
733-
}
734-
713+
async beforeBrowserStop() {
735714
if (this.chrome.collectNetLog && !this.chrome.android) {
736715
const netlog = `${this.baseDir}/chromeNetlog.json`;
737716
await unlink(netlog);

‎lib/chrome/webdriver/setupChromiumOptions.js‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ const log = getLogger('browsertime.chrome');
99

1010
// Chromedriver only knows Chrome by its package name. For other Chromium
1111
// browsers on Android it needs the name of the DevTools socket, and the
12-
// browser reads its flags from its own file. keepAppData: the browser shows a
13-
// welcome screen after its data is cleared, so we keep the data and clear the
14-
// cache and cookies instead.
12+
// browser reads its flags from its own file. keepAppData: Chromedriver keeps
13+
// the data, because Browsertime clears it itself, see lib/android/edge.js.
1514
const OTHER_ANDROID_BROWSERS = {
1615
'com.microsoft.emmx': {
1716
activity: 'com.microsoft.ruby.Main',
@@ -360,9 +359,3 @@ export function getAndroidDevtoolsSocket(browserOptions) {
360359
'chrome_devtools_remote'
361360
);
362361
}
363-
364-
export function keepsAndroidAppData(browserOptions) {
365-
return Boolean(
366-
OTHER_ANDROID_BROWSERS[browserOptions.android?.package]?.keepAppData
367-
);
368-
}

‎lib/edge/webdriver/builder.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
setupChromiumOptions,
66
writeAndroidCommandLine
77
} from '../../chrome/webdriver/setupChromiumOptions.js';
8+
import { AndroidEdge } from '../../android/edge.js';
89
import { pac, manual } from 'selenium-webdriver/proxy.js';
910
import { pick, isEmpty, getProperty } from '../../support/util.js';
1011
const log = getLogger('browsertime.edge');
@@ -86,6 +87,9 @@ export async function configureBuilder(builder, baseDir, options, tmpDir) {
8687

8788
setupChromiumOptions(edgeOptions, chromeConfig, options, baseDir);
8889
await writeAndroidCommandLine(chromeConfig, options);
90+
if (AndroidEdge.isConfigured(chromeConfig)) {
91+
await new AndroidEdge(options).startWithCleanProfile();
92+
}
8993

9094
builder.setEdgeOptions(edgeOptions);
9195
}

0 commit comments

Comments
 (0)