Skip to content

Commit b5bd031

Browse files
Lightning00BladeDevtools-frontend LUCI CQ
authored andcommitted
[e2e] Try to restart the browser if it's disconnected
When the browser crashes the underlying Transport (WebSockets) should close, so we can look at this signal, and try to create a new browser. For this we need to try to get/check the browser before each test, using maps to reduce unnecessary check in information. Also as drive-by clean up some extra issue: When we try to get screenshot with from closed browser. When we try to close a closed browser. Fixed: 430539152 Change-Id: I7c1c3d2037beb6e689d888027303935e5d8b4a4c Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6718828 Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Nikolay Vitkov <[email protected]> Reviewed-by: Philip Pfaffe <[email protected]> Auto-Submit: Nikolay Vitkov <[email protected]>
1 parent 5712fc8 commit b5bd031

File tree

4 files changed

+65
-24
lines changed

4 files changed

+65
-24
lines changed

test/e2e_non_hosted/conductor/mocha-interface-helpers.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import {TestConfig} from '../../conductor/test_config.js';
1111

1212
import {StateProvider} from './state-provider.js';
1313

14-
async function takeScreenshots(state: E2E.State): Promise<{target?: string, frontend?: string}> {
14+
async function takeScreenshots(state: E2E.State): Promise<{inspectedPage?: string, devToolsPage?: string}> {
1515
try {
1616
const {devToolsPage, inspectedPage} = state;
17-
const targetScreenshot = await inspectedPage.screenshot();
18-
const frontendScreenshot = await devToolsPage.screenshot();
19-
return {target: targetScreenshot, frontend: frontendScreenshot};
17+
const inspectedPageScreenshot = await inspectedPage.screenshot();
18+
const devToolsPageScreenshot = await devToolsPage.screenshot();
19+
return {inspectedPage: inspectedPageScreenshot, devToolsPage: devToolsPageScreenshot};
2020
} catch (err) {
2121
console.error('Error taking a screenshot', err);
2222
return {};
@@ -39,12 +39,17 @@ async function finalizeTestError(state: ExtendedState|undefined, error: Error):
3939
}
4040

4141
export async function screenshotError(state: E2E.State, error: Error) {
42+
if (!state.browser.connected) {
43+
console.error('Browser was disconnected, skipping screenshots');
44+
return error;
45+
}
46+
4247
console.error('Taking screenshots for the error:', error);
4348
if (!TestConfig.debug) {
4449
try {
4550
const screenshotTimeout = 5_000;
4651
let timer: ReturnType<typeof setTimeout>;
47-
const {target, frontend} = await Promise.race([
52+
const {inspectedPage, devToolsPage} = await Promise.race([
4853
takeScreenshots(state).then(result => {
4954
clearTimeout(timer);
5055
return result;
@@ -53,10 +58,10 @@ export async function screenshotError(state: E2E.State, error: Error) {
5358
timer = setTimeout(resolve, screenshotTimeout);
5459
}).then(() => {
5560
console.error(`Could not take screenshots within ${screenshotTimeout}ms.`);
56-
return {target: undefined, frontend: undefined};
61+
return {inspectedPage: undefined, devToolsPage: undefined};
5762
}),
5863
]);
59-
return ScreenshotError.fromBase64Images(error, target, frontend);
64+
return ScreenshotError.fromBase64Images(error, inspectedPage, devToolsPage);
6065
} catch (e) {
6166
console.error('Unexpected error saving screenshots', e);
6267
return e;
@@ -121,7 +126,9 @@ export class InstrumentedTestFunction {
121126
// including all error and timeout handling that still might rely
122127
// on the browserContext and pages.
123128
try {
124-
await this.state?.browsingContext.close();
129+
if (this.state?.state.browser.connected) {
130+
await this.state?.browsingContext.close();
131+
}
125132
} catch (e) {
126133
console.error('Unexpected error during cleanup', e);
127134
}

test/e2e_non_hosted/conductor/mocha-interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function devtoolsTestInterface(rootSuite: Mocha.Suite) {
4949
mochaGlobals.describe = customDescribe(defaultImplementation.suite, '', thisSuite);
5050
// @ts-expect-error Custom interface.
5151
mochaGlobals.setup = function(suiteSettings: SuiteSettings) {
52-
StateProvider.instance.registerSettingsCallback(thisSuite, suiteSettings);
52+
StateProvider.instance.registerSuiteSettings(thisSuite, suiteSettings);
5353
};
5454
// @ts-expect-error Custom interface.
5555
mochaGlobals.it = customIt(defaultImplementation.test, thisSuite, thisSuite.file || '', mochaRoot);
@@ -67,7 +67,7 @@ function devtoolsTestInterface(rootSuite: Mocha.Suite) {
6767
});
6868

6969
if (!suite.isPending()) {
70-
suite.beforeAll(async function(this: Mocha.Context) {
70+
suite.beforeEach(async function(this: Mocha.Context) {
7171
this.timeout(0);
7272
await StateProvider.instance.resolveBrowser(suite);
7373
});

test/e2e_non_hosted/conductor/state-provider.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,71 @@ const DEFAULT_SETTINGS = {
1919

2020
export class StateProvider {
2121
static instance = new StateProvider();
22-
23-
#settingsCallbackMap = new Map<Mocha.Suite, E2E.SuiteSettings>();
24-
#browserMap = new Map<string, BrowserWrapper>();
2522
static serverPort: number;
2623

24+
/**
25+
* This provides a mapping whenever we have called the
26+
* {@link Mocha.setup | setup()} function with custom settings.
27+
*/
28+
#suiteToSettingsMap = new WeakMap<Mocha.Suite, E2E.SuiteSettings>();
29+
/**
30+
* This provides a quick link between suite and browser with
31+
* the correct setting allowing us to skip some check
32+
* because we try to run the creation of browser in a beforeEach
33+
*/
34+
#suiteToBrowser = new WeakMap<Mocha.Suite, BrowserWrapper>();
35+
/**
36+
* Provides a mapping between a stable key (sorted JSON)
37+
* created from the settings and a browser.
38+
* This allows us to have a single browser between test
39+
* that require the same settings.
40+
*/
41+
#settingToBrowser = new Map<string, BrowserWrapper>();
42+
2743
private constructor() {
2844
}
2945

30-
registerSettingsCallback(suite: Mocha.Suite, suiteSettings: E2E.SuiteSettings) {
31-
this.#settingsCallbackMap.set(suite, suiteSettings);
46+
registerSuiteSettings(suite: Mocha.Suite, suiteSettings: E2E.SuiteSettings): void {
47+
this.#suiteToSettingsMap.set(suite, suiteSettings);
3248
}
3349

34-
async resolveBrowser(suite: Mocha.Suite) {
50+
async resolveBrowser(suite: Mocha.Suite): Promise<void> {
3551
if (!StateProvider.serverPort) {
3652
StateProvider.serverPort = await StateProvider.#globalSetup();
3753
}
38-
const settings = await this.#getSettings(suite);
54+
let browser = this.#suiteToBrowser.get(suite);
55+
if (browser?.connected) {
56+
return;
57+
}
58+
59+
const settings = this.#getSettings(suite);
3960
const browserSettings = {
4061
enabledBlinkFeatures: (settings.enabledBlinkFeatures ?? []).toSorted(),
4162
disabledFeatures: (settings.disabledFeatures ?? []).toSorted(),
4263
};
4364
const browserKey = JSON.stringify(browserSettings);
44-
let browser = this.#browserMap.get(browserKey);
45-
if (!browser) {
65+
browser = this.#settingToBrowser.get(browserKey);
66+
if (browser && !browser.connected) {
67+
browser?.browser.process()?.kill();
68+
}
69+
70+
if (!browser?.connected) {
4671
browser = await Launcher.browserSetup(browserSettings);
47-
this.#browserMap.set(browserKey, browser);
72+
this.#settingToBrowser.set(browserKey, browser);
4873
}
74+
this.#suiteToBrowser.set(suite, browser);
4975
// Suite needs to be aware of the browser instance to be able to create the
5076
// full state for the tests
5177
suite.browser = browser;
5278
}
5379

5480
async getState(suite: Mocha.Suite) {
55-
const settings = await this.#getSettings(suite);
81+
const settings = this.#getSettings(suite);
5682
const browser = suite.browser;
83+
if (!browser.connected) {
84+
throw new Error('Browser disconnected unexpectedly');
85+
}
86+
5787
const browsingContext = await browser.createBrowserContext();
5888
const inspectedPage = await setupInspectedPage(browsingContext, StateProvider.serverPort);
5989
const devToolsPage = await setupDevToolsPage(browsingContext, settings);
@@ -68,8 +98,8 @@ export class StateProvider {
6898
return {state, browsingContext};
6999
}
70100

71-
async #getSettings(suite: Mocha.Suite): Promise<E2E.HarnessSettings> {
72-
const settings = this.#settingsCallbackMap.get(suite);
101+
#getSettings(suite: Mocha.Suite): E2E.HarnessSettings {
102+
const settings = this.#suiteToSettingsMap.get(suite);
73103
if (settings) {
74104
return mergeSettings(settings, DEFAULT_SETTINGS);
75105
}
@@ -88,7 +118,7 @@ export class StateProvider {
88118
}
89119

90120
async closeBrowsers() {
91-
await Promise.all([...this.#browserMap.values()].map(async browser => {
121+
await Promise.allSettled([...this.#settingToBrowser.values()].map(async browser => {
92122
await browser.browser.close();
93123
}));
94124
}

test/e2e_non_hosted/shared/browser-helper.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export class BrowserWrapper {
1717
this.browser = b;
1818
}
1919

20+
get connected() {
21+
return this.browser.connected;
22+
}
23+
2024
async createBrowserContext() {
2125
return await this.browser.createBrowserContext();
2226
}

0 commit comments

Comments
 (0)