blob: 4c776b71aa52bc35db051f3284cd22200d615510 [file] [log] [blame] [view]
Rebekah Potter1ebb97f2023-10-25 15:38:451<style>
2.note::before {
3 content: 'Note: ';
4 font-variant: small-caps;
5 font-style: italic;
6}
7
8.doc h1 {
9 margin: 0;
10}
11</style>
12
13# Testing WebUI Pages
14
dpapad543280c2025-01-24 08:03:2515[TOC]
16
Rebekah Potter1ebb97f2023-10-25 15:38:4517Chromium WebUI pages should be tested using Mocha test suites.
18[Mocha](https://mochajs.org) is a flexible, lightweight test framework with
19strong async support. With Mocha, we can write small unit tests for individual
20features of components instead of grouping everything into a monolithic browser
21test.
22
23There are a few pieces of Chromium WebUI infrastructure designed to make
24setting up WebUI Mocha browser tests easier:
25
26* `build_webui_tests` is a gn build rule that preprocesses test `.ts` files,
27 compiles them to JavaScript, and places them in a `.grdp` file. This file
28 can then be included in the WebUI test `.grd` so that the files can be
29 served at runtime. This rule is further documented at
30 [WebUI Build Configuration](./webui_build_configuration.md)
31* `WebUIMochaBrowserTest` is a test base class that WebUI tests should
32 inherit from. It handles browser test setup and teardown, injects Mocha,
33 loads the `chrome://` page being tested, and imports the test files. This
34 class also ensures code coverage metrics are collected when JS code
35 coverage is enabled. The API for this class is documented in its
36 [header
37 file](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/base/web_ui_mocha_browser_test.h)
38
39Note: For the default test loading logic in `WebUIMochaBrowserTest` to work,
40`test_loader.html`, `test_loader_util.js` and `test_loader.js` should be
41registered with the production Web UI's data source. This can be done manually,
42or (most commonly) can be done by calling `SetupWebUIDataSource()`. If using
43the test loader is not feasible for a given test - for example, test relies on
44loading a specific URL such that loading `chrome://foo-bar/test_loader.html`
45would break the test - see [Unusual Test Setups](#unusual-test-setups)
46
47### Adding the BrowserTest
48UIs should add a `.cc` file with the definition of their BrowserTest class in
49`chrome/test/data/webui/my_webui_dir_name`. The directory structure
50of `chrome/test/data/webui` should mirror that of
51`chrome/browser/resources/` where the WebUI TypeScript/HTML code being tested
52resides. The BrowserTest class defined in the `.cc` file should inherit from
53`WebUIMochaBrowserTest`.
54Example:
55
56```c++
57// chrome/test/data/webui/foo_bar/foo_bar_browsertest.cc
58
59#include "base/test/scoped_feature_list.h"
60#include "chrome/common/webui_url_constants.h"
61#include "chrome/test/base/web_ui_mocha_browser_test.h"
62#include "content/public/test/browser_test.h"
63
64class FooBarBrowserTest : public WebUIMochaBrowserTest {
65 protected:
66 FooBarBrowserTest() {
67 set_test_loader_host(chrome::kChromeUIFooBarHost);
68 }
69
70 private:
71 // Necessary if you have features (or the UI itself) behind a feature flag.
72 base::test::ScopedFeatureList scoped_feature_list_{foo_bar::kSomeNewFeature};
73};
Rebekah Potter1ebb97f2023-10-25 15:38:4574```
75
Rebekah Potter9ccc1d822023-11-06 23:58:4176Note: If testing a `chrome-untrusted://` UI, also call:
dpapad994a9392025-03-25 00:21:4377```c++
Rebekah Potter9ccc1d822023-11-06 23:58:4178set_test_loader_scheme(content::kChromeUIUntrustedScheme);
79```
80in the constructor.
81
Rebekah Potter1ebb97f2023-10-25 15:38:4582After defining the class, create one or more `IN_PROC_BROWSER_TEST_F`
83functions. Each should run one or more Mocha tests.
84
85``` c++
86typedef FooBarBrowserTest FooBarAppTest;
87IN_PROC_BROWSER_TEST_F(FooBarAppTest, All) {
88 // Run all Mocha test suites found in app_test
89 RunTest("foo_bar/app_test.js", "mocha.run();");
90}
Rebekah Potter1ebb97f2023-10-25 15:38:4591```
92Should you write a large number of tests in a single file (e.g., for
93one particular custom element), it may be necessary to run different suites or
94even individual tests from that same file in separate `IN_PROC_BROWSER_TEST_F`
95invocations, to avoid having one very long test that is at risk of timing out.
96
97Example:
98
99```c++
100// This class defines a convenience method to run one Mocha test suite in the
101// search_box_test.js file at a time.
102class FooBarSearchBoxTest: public FooBarBrowserTest {
103 protected:
104 void RunTestSuite(const std::string& suiteName) {
105 FooBarBrowserTest::RunTest(
106 "foo_bar/search_box_test.js",
107 base::StringPrintf("runMochaSuite('%s');", suiteName.c_str()));
108 }
109};
110
111IN_PROC_BROWSER_TEST_F(FooBarSearchBoxTest, SearchNonEmpty) {
112 RunTestSuite("SearchNonEmpty");
113}
114
115IN_PROC_BROWSER_TEST_F(FooBarSearchBoxTest, SearchEmpty) {
116 RunTestCase("SearchEmpty");
117}
118
119// This class defines a convenience method to run one Mocha test from the
120// FooBarChildDialogTest suite at a time.
121class FooBarChildDialogTest: public FooBarBrowserTest
122 protected:
123 void RunTestCase(const std::string& testCase) {
124 FooBarBrowserTest::RunTest(
125 "foo_bar/child_dialog_test.js",
126 base::StringPrintf("runMochaTest('FooBarChildDialogTest', '%s');",
127 testCase.c_str()));
128 }
129};
130
131IN_PROC_BROWSER_TEST_F(FooBarChildDialogTest, OpenDialog) {
132 RunTestCase("OpenDialog");
133}
134
135IN_PROC_BROWSER_TEST_F(FooBarChildDialogTest, ClickButtons) {
136 RunTestCase("ClickButtons");
137}
138```
139
140### Adding the Mocha test files
141Mocha test files should be added alongside the `.cc` file in the same
142directory. Each test file should contain one or more Mocha test suites. Often
143each major custom element of a UI will have its own Mocha test file, as will
144many of the data classes.
145
dpapad543280c2025-01-24 08:03:25146```ts
Rebekah Potter1ebb97f2023-10-25 15:38:45147// chrome/test/data/webui/foo_bar/search_box_test.ts
148
149import 'chrome://foo-bar/foo_bar.js';
150
151import {FooBarSearchBoxElement} from 'chrome://foo-bar/foo_bar.js';
152import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
153import {eventToPromise} from 'chrome://webui-test/test_util.js';
154
155suite('SearchNonEmpty', function() {
156 let searchBox: FooBarSearchBoxElement;
157
158 setup(function() {
159 document.body.innerHTML = window.trustedTypes!.emptyHTML;
160 const searchBox = document.createElement('foo-bar-search-box');
161 document.body.appendChild(searchBox);
162
163 // More setup goes here
164 });
165
166 test('search abc', async() {
167 const search = searchBox.shadowRoot!.querySelector('input');
168 assertTrue(!!search);
169 search.value = 'abc';
170 await eventToPromise('search-change', searchBox);
171
172 // Do some assertions
173 });
174
175 test('search def', async () => {
176 // etc
177 });
178});
179
180suite('SearchEmpty', function() {
181 let searchBox: FooBarSearchBoxElement;
182
183 setup(function() {
184 document.body.innerHTML = window.trustedTypes!.emptyHTML;
185 const searchBox = document.createElement('foo-bar-search-box');
186 document.body.appendChild(searchBox);
187
188 // Different setup goes here
189 });
190
191 test('search abc when empty', async() {
192 const search = searchBox.shadowRoot!.querySelector('input');
193 assertTrue(!!search);
194 search.value = 'abc';
195 await eventToPromise('search-change', searchBox);
196
197 // Do some assertions
198 });
199});
200
201```
202### Setting up the build
203To build your tests as part of the `browser_tests` target, add a BUILD.gn file
204in your folder:
205
dpapad994a9392025-03-25 00:21:43206```python
Rebekah Potter1ebb97f2023-10-25 15:38:45207# chrome/test/data/webui/foo_bar/BUILD.gn
208
209import("../build_webui_tests.gni")
210
211assert(!is_android)
212
213build_webui_tests("build") {
214 # Test files go here
215 files = [
216 "app_test.ts",
217 "child_dialog_test.ts",
218 "search_box_test.ts",
219 ]
220
221 # Path mapping the WebUI host URL to the path where the WebUI's compiled
222 # TypeScript files are placed by the TS compiler.
223 ts_path_mappings =
224 [ "chrome://foo-bar/*|" +
225 rebase_path("$root_gen_dir/chrome/browser/resources/foo_bar/tsc/*",
226 target_gen_dir) ]
227 ts_deps = [
228 "//chrome/browser/resources/foo_bar:build_ts",
229 ]
230}
231```
232
Rebekah Potter9ccc1d822023-11-06 23:58:41233Note: If testing a `chrome-untrusted://` UI, also set the
234`is_chrome_untrusted` parameter for `build_webui_tests()` to `true`, to allow
235importing shared test files from `chrome-untrusted://webui-test/` paths.
236
Rebekah Potter1ebb97f2023-10-25 15:38:45237You then need to hook up the targets generated by this file, and list your
238browsertest.cc source file, in `chrome/test/data/webui/BUILD.gn`:
239
dpapad994a9392025-03-25 00:21:43240```python
Rebekah Potter1ebb97f2023-10-25 15:38:45241# chrome/test/data/webui/BUILD.gn
242
243source_set("browser_tests") {
244 testonly = true
245
246 sources = [
247 # Lots of files
248 # Big list, add your .cc file in alphabetical order
249 "foo_bar/foo_bar_browsertest.cc",
250 # Lots more files
251 ]
252
253 # Conditionally added sources (e.g., platform-specific)
254
255}
256
257# Further down in the file...
258
259generate_grd("build_grd") {
260 testonly = true
261 grd_prefix = "webui_test"
262 output_files_base_dir = "test/data/grit"
263 out_grd = "$target_gen_dir/resources.grd"
264
265 deps = [
266 ":build_chai_grdp",
267 ":build_mocha_grdp",
268 # Lots of other deps here
269 # Add your build_grdp target:
270 "foo_bar:build_grdp",
271 # Lots more other deps
272 ]
273
274 grdp_files = [
275 # Big list of grdp files
276 # Add your grdp in alphabetical order
277 "$target_gen_dir/foo_bar/resources.grdp",
278 # Lots more grdp files
279 ]
280
281 # Conditionally added (e.g. platform-specific) grdps and deps go here
282}
Rebekah Potter1ebb97f2023-10-25 15:38:45283```
dpapad994a9392025-03-25 00:21:43284
Rebekah Potter1ebb97f2023-10-25 15:38:45285### Unusual Test Setups
286Some UIs may need to rely on loading a particular URL, such that using the
287default `chrome://foo-bar/test_loader.html` URL that is used to dynamically
288import the test modules may not work. Other UIs need to do additional setup
289on the C++ side of the test, and simply want to run a test on a specified
290WebContents instance. These cases are accommodated by additional methods in
291`WebUIMochaBrowserTest`. A few brief examples are provided below.
292
293Example: loading a different URL
294```c++
295IN_PROC_BROWSER_TEST_F(FooBarBrowserTest, NonExistentUrl) {
296 // Invoking the test from a non existent URL chrome://foo-bar/does-not-exist/.
297 set_test_loader_host(
298 std::string(chrome::kChromeFooBarHost) + "/does-not-exist");
299 RunTestWithoutTestLoader("foo_bar/non_existent_url_test.js", "mocha.run()");
300}
301```
302
303Example: Advanced setup required in C++
304```c++
305IN_PROC_BROWSER_TEST_F(FooDialogBrowserTest, MyTest) {
306 ui_test_utils::NavigateToURLWithDisposition(
307 browser(), GURL(chrome::kSomeOtherBaseUrl),
308 WindowOpenDisposition::CURRENT_TAB,
309 ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
310 FooDialog* my_dialog = FooDialog::ShowConstrained(
311 kDummyParam,
312 browser()->tab_strip_model()->GetActiveWebContents(),
313 browser()->window()->GetNativeWindow());
314 content::WebContents* web_contents = my_dialog->webui_->GetWebContents();
315
316 ASSERT_TRUE(RunTestOnWebContents(
317 web_contents, "foo_dialog/foo_dialog_test.js", "mocha.run()",
318 /*skip_test_loader=*/true));
319}
320```
Erik Chen382a20852024-08-22 09:51:35321
dpapad543280c2025-01-24 08:03:25322### Running tests
323
324You can build the `browser_tests` target and run your WebUI tests just like
325other browser tests. You may filter tests by class or test name (the arguments
326to `IN_PROC_BROWSER_TEST_F`) via `--gtest_filter`:
327
328```sh
329autoninja -C out/Release browser_tests
330./out/Release/browser_tests --gtest_filter=FooBarSearchBoxTest.Empty
331```
332
333### Debugging tests
334
335You can run a subset of the Mocha test cases, by using `test.only()` instead of
336`test()`. For example:
337
338```ts
339test.only('MyTestcase', function() {...});
340```
341
342You can skip a subset of the Mocha test cases, by using `test.skip()` instead of
343`test()`. For example:
344
345```ts
346test.skip('MyTestcase', function() {...});
347```
348
349When authoring a new test case or when investigating existing test failures, you
350will often have a need to step through the code in the DevTools debugger, to
351figure out what is happening. Use the `launchDebugger` helper function to do
352that. Detailed instructions on how to use it exist in the source code
353documentation [here] (https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/data/webui/test_util.ts;l=107-144;drc=b1866df4398a971088ba287d4c7efe704f6bc4b1)
354
Alex Yang04904912025-04-16 16:58:33355### Reporting WebUI test results to LUCI
356
357WebUI test results are automatically reported to LUCI. No special handling is
358required of WebUI test authors.
359
360A single GTest typically reports a single test result. A [new test reporting
361mechanism was
362added](https://chromium-review.googlesource.com/c/chromium/src/+/6358478) to
363support reporting multiple results from a single GTest. Using this mechanism,
364each Mocha JS test can have its own entry in LUCI (see screenshot). The
365integration is accomplished by calling base::AddSubTestResult in
366WebUIMochaBrowserTest.
367
368![screenshot of WebUI JS test results in LUCI UI](images/luci_webui_js_test_results.png)
369
Erik Chen382a20852024-08-22 09:51:35370### Common errors
371
372Tests that rely on focus, blur, or other input-related events need to be added
373to the interactive_ui_tests build target rather than the browser_tests target.
374browser_tests are run in parallel, and the window running the test will
375gain/lose activation at unpredictable times.
376
377Tests that touch production logic that depends on onFocus, onBlur or similar
378events also needs to be added to interactive_ui_tests