Skip to content

Commit 34154dd

Browse files
Liviu RauDevtools-frontend LUCI CQ
Liviu Rau
authored and
Devtools-frontend LUCI CQ
committed
Implement non-hosted mode test harness
NO_IFTTT: getting in sync with older changes Bug: 390535614 Change-Id: I807dc1686a4a052e49ff484db75e9b6638772719 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6278526 Commit-Queue: Liviu Rau <liviurau@chromium.org> Reviewed-by: Alex Rudenko <alexrudenko@chromium.org> Reviewed-by: Philip Pfaffe <pfaffe@chromium.org> Reviewed-by: Simon Zünd <szuend@chromium.org>
1 parent 3dd344a commit 34154dd

23 files changed

+973
-26
lines changed

test/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ group("test") {
1010
":run",
1111
":unittests",
1212
"e2e",
13+
"e2e_non_hosted",
1314
"interactions",
1415
"perf",
1516
"shared",

test/conductor/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ node_ts_library("implementation") {
2525
"mocha-interface.ts",
2626
"mocha_hooks.ts",
2727
"paths.ts",
28+
"platform.ts",
2829
"pool.ts",
2930
"puppeteer-state.ts",
3031
"resultsdb.ts",
3132
"screenshot-error.ts",
33+
"server_port.ts",
3234
"target_tab.ts",
3335
"test_config.ts",
3436
"test_server.ts",

test/conductor/hooks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import {
2121
getBrowserAndPages,
2222
registerHandlers,
2323
setBrowserAndPages,
24-
setTestServerPort,
2524
} from './puppeteer-state.js';
25+
import {setTestServerPort} from './server_port.js';
2626
import {TargetTab} from './target_tab.js';
2727
import {TestConfig} from './test_config.js';
2828

@@ -55,6 +55,7 @@ const envChromeFeatures = process.env['CHROME_FEATURES'];
5555

5656
function launchChrome() {
5757
// Use port 0 to request any free port.
58+
// LINT.IfChange(features)
5859
const enabledFeatures = [
5960
'Portals',
6061
'PortalsCrossOrigin',
@@ -64,13 +65,13 @@ function launchChrome() {
6465
'PrivacySandboxAdsAPIsOverride',
6566
'AutofillEnableDevtoolsIssues',
6667
];
67-
6868
const disabledFeatures = [
6969
'PMProcessPriorityPolicy', // crbug.com/361252079
7070
'MojoChannelAssociatedSendUsesRunOrPostTask', // crbug.com/376228320
7171
'RasterInducingScroll', // crbug.com/381055647
7272
'CompositeBackgroundColorAnimation', // crbug.com/381055647
7373
];
74+
// LINT.ThenChange(/test/e2e_non_hosted/shared/browser-helper.ts:features)
7475
const launchArgs = [
7576
'--remote-allow-origins=*',
7677
'--remote-debugging-port=0',

test/conductor/platform.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as os from 'os';
6+
7+
export type Platform = 'mac'|'win32'|'linux';
8+
export let platform: Platform;
9+
switch (os.platform()) {
10+
case 'darwin':
11+
platform = 'mac';
12+
break;
13+
14+
case 'win32':
15+
platform = 'win32';
16+
break;
17+
18+
default:
19+
platform = 'linux';
20+
break;
21+
}

test/conductor/puppeteer-state.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@
55
import * as puppeteer from 'puppeteer-core';
66

77
import {querySelectorShadowTextAll, querySelectorShadowTextOne} from './custom-query-handlers.js';
8+
import {clearServerPort} from './server_port.js';
89

910
let target: puppeteer.Page|null;
1011
let frontend: puppeteer.Page|null;
1112
let browser: puppeteer.Browser|null;
1213

13-
// Set when we launch the server. It will be different for each
14-
// sub-process runner when running in parallel.
15-
let testServerPort: number|null;
16-
1714
export interface BrowserAndPages {
1815
target: puppeteer.Page;
1916
frontend: puppeteer.Page;
@@ -24,7 +21,7 @@ export const clearPuppeteerState = () => {
2421
target = null;
2522
frontend = null;
2623
browser = null;
27-
testServerPort = null;
24+
clearServerPort();
2825
};
2926

3027
export const setBrowserAndPages = (newValues: BrowserAndPages) => {
@@ -55,23 +52,6 @@ export const getBrowserAndPages = (): BrowserAndPages => {
5552
};
5653
};
5754

58-
export const setTestServerPort = (port: number) => {
59-
if (testServerPort) {
60-
throw new Error('Can\'t set the test server port twice.');
61-
}
62-
testServerPort = port;
63-
};
64-
65-
export const getTestServerPort = () => {
66-
if (!testServerPort) {
67-
throw new Error(
68-
'Unable to locate test server port. Was it stored first?' +
69-
'\nYou might be calling this function at module instantiation time, instead of ' +
70-
'at runtime when the port is available.');
71-
}
72-
return testServerPort;
73-
};
74-
7555
let handlerRegistered = false;
7656
export const registerHandlers = () => {
7757
if (handlerRegistered) {

test/conductor/server_port.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
// Copyright 2025 The Chromium Authors. All rights reserved.
3+
// Use of this source code is governed by a BSD-style license that can be
4+
// found in the LICENSE file.
5+
6+
// Set when we launch the server. It will be different for each
7+
// sub-process runner when running in parallel.
8+
let testServerPort: number|null;
9+
10+
export const setTestServerPort = (port: number) => {
11+
if (testServerPort) {
12+
throw new Error('Can\'t set the test server port twice.');
13+
}
14+
testServerPort = port;
15+
};
16+
17+
export const getTestServerPort = () => {
18+
if (!testServerPort) {
19+
throw new Error(
20+
'Unable to locate test server port. Was it stored first?' +
21+
'\nYou might be calling this function at module instantiation time, instead of ' +
22+
'at runtime when the port is available.');
23+
}
24+
return testServerPort;
25+
};
26+
27+
export function clearServerPort() {
28+
testServerPort = null;
29+
}

test/conductor/test_server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function startServer(server: 'hosted-mode'|'component-docs', commandLineA
4949
});
5050
}
5151

52+
process.on('exit', stopServer);
5253
export function stopServer() {
5354
runningServer.kill();
5455
}

test/e2e_non_hosted/BUILD.gn

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2020 The Chromium Authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
import("../../scripts/build/ninja/copy.gni")
6+
import("../../scripts/build/typescript/typescript.gni")
7+
8+
group("e2e_non_hosted") {
9+
deps = [
10+
":test_inputs",
11+
":tests",
12+
]
13+
}
14+
15+
node_ts_library("tests") {
16+
deps = [
17+
"../conductor:implementation",
18+
"dummy",
19+
]
20+
sources = [ "mocharc.ts" ]
21+
}
22+
23+
generated_file("test_inputs") {
24+
outputs = [ "$target_gen_dir/tests.txt" ]
25+
data_keys = [ "tests" ]
26+
rebase = target_gen_dir
27+
28+
deps = [ ":tests" ]
29+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2020 The Chromium Authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
import("../../../scripts/build/typescript/typescript.gni")
6+
7+
group("conductor") {
8+
deps = [
9+
":implementation",
10+
"../../../scripts/component_server",
11+
"../../../scripts/hosted_mode",
12+
]
13+
}
14+
15+
node_ts_library("implementation") {
16+
deps = [
17+
"../../conductor:implementation",
18+
"../shared",
19+
]
20+
sources = [
21+
"mocha-interface-helpers.ts",
22+
"mocha-interface.ts",
23+
"state-provider.ts",
24+
]
25+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type * as Mocha from 'mocha';
6+
7+
import {AsyncScope} from '../../conductor/async-scope.js';
8+
import type {Platform} from '../../conductor/platform.js';
9+
import {ScreenshotError} from '../../conductor/screenshot-error.js';
10+
import {TestConfig} from '../../conductor/test_config.js';
11+
import type {BrowserSettings, BrowserWrapper} from '../shared/browser-helper.js';
12+
import type {DevToolsFronendPage, DevtoolsSettings} from '../shared/frontend-helper.js';
13+
import type {InspectedPage} from '../shared/target-helper.js';
14+
declare global {
15+
namespace Mocha {
16+
export interface TestFunction {
17+
(title: string, fn: TestCallbackWithState): void;
18+
19+
skipOnPlatforms: (platforms: Platform[], title: string, fn: Mocha.AsyncFunc) => void;
20+
}
21+
export interface Suite {
22+
settings: SuiteSettings;
23+
state: State;
24+
browser: BrowserWrapper;
25+
}
26+
}
27+
}
28+
29+
export type HarnessSettings = BrowserSettings&DevtoolsSettings;
30+
export type SuiteSettings = Partial<HarnessSettings>;
31+
32+
export interface State {
33+
devToolsPage: DevToolsFronendPage;
34+
inspectedPage: InspectedPage;
35+
browser: BrowserWrapper;
36+
}
37+
38+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
39+
export type TestCallbackWithState = (state: State) => PromiseLike<any>;
40+
41+
async function takeScreenshots(state: State): Promise<{target?: string, frontend?: string}> {
42+
try {
43+
const {devToolsPage, inspectedPage} = state;
44+
const targetScreenshot = await inspectedPage.screenshot();
45+
const frontendScreenshot = await devToolsPage.screenshot();
46+
return {target: targetScreenshot, frontend: frontendScreenshot};
47+
} catch (err) {
48+
console.error('Error taking a screenshot', err);
49+
return {};
50+
}
51+
}
52+
53+
async function createScreenshotError(test: Mocha.Runnable|undefined, error: Error): Promise<Error> {
54+
if (!test?.parent?.state) {
55+
console.error('Missing browsing state. Unable to take screenshots for the error:', error);
56+
return error;
57+
}
58+
const sate = test.parent.state;
59+
console.error('Taking screenshots for the error:', error);
60+
if (!TestConfig.debug) {
61+
try {
62+
const screenshotTimeout = 5_000;
63+
let timer: NodeJS.Timeout;
64+
const {target, frontend} = await Promise.race([
65+
takeScreenshots(sate).then(result => {
66+
clearTimeout(timer);
67+
return result;
68+
}),
69+
new Promise(resolve => {
70+
timer = setTimeout(resolve, screenshotTimeout);
71+
}).then(() => {
72+
console.error(`Could not take screenshots within ${screenshotTimeout}ms.`);
73+
return {target: undefined, frontend: undefined};
74+
}),
75+
]);
76+
return ScreenshotError.fromBase64Images(error, target, frontend);
77+
} catch (e) {
78+
console.error('Unexpected error saving screenshots', e);
79+
return e;
80+
}
81+
}
82+
return error;
83+
}
84+
85+
export function makeInstrumentedTestFunction(fn: Mocha.AsyncFunc, label: string) {
86+
return async function testFunction(this: Mocha.Context) {
87+
const abortController = new AbortController();
88+
let resolver;
89+
let rejecter: (reason?: unknown) => void;
90+
const testPromise = new Promise((resolve, reject) => {
91+
resolver = resolve;
92+
rejecter = reject;
93+
});
94+
// AbortSignal for the current test function.
95+
AsyncScope.abortSignal = abortController.signal;
96+
// Promisify the function in case it is sync.
97+
const promise = (async () => await fn.call(this))();
98+
const actualTimeout = this.timeout();
99+
// Disable test timeout.
100+
this.timeout(0);
101+
const t = actualTimeout !== 0 ? setTimeout(async () => {
102+
abortController.abort();
103+
const stacks = [];
104+
const scopes = AsyncScope.scopes;
105+
for (const scope of scopes.values()) {
106+
const {descriptions, stack} = scope;
107+
if (stack) {
108+
const stepDescription = descriptions ? `${descriptions.join(' > ')}:\n` : '';
109+
stacks.push(`${stepDescription}${stack.join('\n')}\n`);
110+
}
111+
}
112+
const err = new Error(`A test function (${label}) for "${this.test?.title}" timed out`);
113+
if (stacks.length > 0) {
114+
const msg = `Pending async operations during timeout:\n${stacks.join('\n\n')}`;
115+
err.cause = new Error(msg);
116+
}
117+
rejecter(await createScreenshotError(this.test, err));
118+
}, actualTimeout) : 0;
119+
promise
120+
.then(
121+
resolver,
122+
async err => {
123+
// Suppress errors after the test was aborted.
124+
if (abortController.signal.aborted) {
125+
return;
126+
}
127+
rejecter(await createScreenshotError(this.test, err));
128+
})
129+
.finally(() => {
130+
clearTimeout(t);
131+
this.timeout(actualTimeout);
132+
});
133+
return await testPromise;
134+
};
135+
}

0 commit comments

Comments
 (0)