| # Using the chrome.test API |
| |
| [TOC] |
| |
| ## What is It? |
| The `chrome.test` extension API is a limited testing framework implemented as |
| an extension API. It is primarily used in order to provide testing |
| functionality used in writing extension API tests by exercising an API directly |
| in JS. See also [writing extension tests]. |
| |
| ### Basic JS-Based Tests |
| All tests must have some limited C++ portion (in order to kick off and drive |
| the test). In the most basic form, this C++ test only needs to load the |
| extension and wait for the response from the testing framework. This is easily |
| accomplished through either `ExtensionApiTest::RunExtensionTest()` or |
| `ExtensionApiTest::RunExtensionSubtest()` (and their variants). |
| |
| `ExtensionApiTest::RunExtensionTest()` loads a test extension and then waits |
| for the result from the testing framework. |
| `ExtensionApiTest::RunExtensionSubtest()` loads a test extension, navigates to |
| a subpage of that extension, and then waits for the result from the testing |
| framework. |
| |
| ### Advanced Tests |
| More advanced tests may require more synchronization between the C++ and the JS |
| (e.g., to set up or verify state on the C++ side before continuing on the JS |
| side). To do this, leverage either `extensions::ResultCatcher`, which waits |
| for the next result from the testing framework (sent using either |
| `chrome.test.notifyPass()`, `chrome.test.notifyFail()`, or waiting for the |
| result of `chrome.test.runTests()`, which calls `notifyPass()` or |
| `notifyFail()` automatically), or `extensions::ExtensionTestMessageListener`, |
| which waits for a message sent by `chrome.test.sendMessage()`. See |
| [writing extension tests] for more information. |
| |
| ## Using the API |
| The API provides a variety of different methods, used for different purposes. |
| |
| ### notifyPass() and notifyFail() |
| `chrome.test.notifyPass()` and `chrome.test.notifyFail()` are used in order to |
| pass the result of running the JS test to the C++. This is the result that |
| `RunExtensionTest()`, `RunExtensionSubtest()`, and the `ResultCatcher` wait on. |
| In order to explicitly pass this result, call `chrome.test.notifyPass()` or |
| `chrome.test.notifyFail()`. |
| |
| ```js |
| chrome.tabs.create(() => { |
| <Verify state> |
| chrome.test.notifyPass(); |
| }); |
| ``` |
| |
| `notifyPass()` and `notifyFail()` may also be implicitly called by the testing |
| API, as described in the sections below. |
| |
| ### test.runTests() |
| `chrome.test.runTests()` is used to run a sequence of individual, smaller JS |
| tests, and then passes the result to the browser by **automatically** calling |
| `chrome.test.notifyPass()` or `chrome.test.notifyFail()`. `notifyPass()` will |
| be called if and only if all individual tests pass; `notifyFail()` will be |
| called if any test fails. A test may fail if an assertion fails, if there is |
| an unexpected runtime error, or if `chrome.test.fail()` is called explicitly. |
| |
| `chrome.test.runTests()` takes an array of functions, and runs them serially. |
| This means that these functions may be independent, or may implicitly rely on |
| one another. The output of running these individual tests is printed through |
| `console.log()`s, which enables tracing how far a test suite progresses. |
| |
| Each individual test function passed to `runTests()` will execute, and then |
| wait for that specific function to pass or fail. Passing is indicated by |
| calling `chrome.test.succeed()` within each test function (**not** |
| `chrome.test.notifyPass()`, which will automatically indicate the entire JS |
| test passes, and may mask failures - see also the [Do's And Don't's]. Failure |
| is indicated by calling `chrome.test.fail()`, a failed assertion, or through |
| an unexpected runtime error or API error (indicated in |
| `chrome.runtime.lastError`). Each test function must signal success or failure; |
| otherwise the test will hang (and eventually timeout). |
| |
| A sample test suite may look like this. |
| |
| ```js |
| let tabId; |
| |
| chrome.test.runTests([ |
| function createNewTab() { |
| chrome.tabs.create({url: 'http://example.com'}, (tab) => { |
| chrome.test.assertNoLastError(); |
| <verify |tab| properties> |
| tabId = tab.id; |
| chrome.test.succeed(); |
| }); |
| }, |
| function queryTab() { |
| chrome.tabs.query({url: 'http://example.com'}, (tabs) => { |
| chrome.test.assertNoLastError(); |
| <verify |tabs|> |
| chrome.test.succeed(); |
| }); |
| }, |
| function removeTab() { |
| chrome.tabs.remove(tabId, () => { |
| chrome.test.assertNoLastError(); |
| chrome.test.succeed(); |
| }); |
| }, |
| ]); |
| ``` |
| |
| ### Assertions |
| The ``chrome.test API`` provides a number of basic assertion methods. |
| |
| #### assertTrue(condition, message?) |
| Asserts that the given condition is true, printing out the optional error |
| message if it is not. |
| |
| #### assertFalse(condition, message?) |
| Asserts that the given condition is false, printing out the optional error |
| message if it is not. |
| |
| #### assertEq(expected, actual, message?) |
| Asserts that the provided value matches the expected value. If `expected` is |
| an object, this will perform a deep-equals check (i.e., verifying that two |
| objects are logically equivalent, rather than have the same address). If the |
| expected value does not match the actual value, this will print out the |
| expected and actual values (through `JSON.stringify()` for objects). |
| |
| #### assertNoLastError() |
| Asserts that `chrome.runtime.lastError` is undefined, printing out the error |
| otherwise. |
| |
| #### assertLastError(expectedError) |
| Asserts that `chrome.runtime.lastError.message` is equivalent to |
| `expectedError`, printing out the expected and actual errors otherwise. |
| |
| #### assertThrows(fn, self?, args[], expectedError?) |
| Asserts that executing `fn` with the context object of `self` (if defined) and |
| the specified `args` array throws a runtime error, which is then validated |
| against `expectedError`. `expectedError` may be either a string (which must |
| match exactly) or a `RegExp`. |
| |
| ### callbackPass() and callbackFail() |
| **Important Notes:** |
| - `callbackPass()` and `callbackFail()` should absolutely **only** be used |
| inside of `chrome.test.runTests()`. |
| - `callbackPass()` and `callbackFail()` are no longer as useful as they were, |
| and sometimes result in less readable, more surprising code. Think twice |
| (or even thrice!) before using them. |
| |
| `callbackPass()` and `callbackFail()` were primarily used when tests needed to |
| wait on two non-deterministic functions. For instance, consider the |
| (contrived) following test: |
| |
| ```js |
| chrome.test.runTests([ |
| function step1() { |
| chrome.tabs.create({url: 'http://example.com'}, () => { |
| <verify state> |
| }); |
| chrome.storage.local.set({foo: 'bar'}, () => { |
| <verify state> |
| }); |
| // Somehow end the test when both the tab creation and storage set have |
| // finished. |
| }, |
| function step2() { |
| ... |
| } |
| ]); |
| ``` |
| |
| Here, we want to have `step1()` finish after both the new tab has been created |
| and a storage value has been set (again, this is admittedly contrived). There |
| is no hard guarantee about which function will finish first, so putting a |
| `chrome.test.succeed()` call in either may result in succeeding and continuing |
| to the next step too early. Putting a `chrome.test.succeed()` call in both will |
| result in badness (see the [Do's And Don't's]). |
| |
| `callbackPass()` lets the testing infrastructure handle this. `callbackPass()` |
| takes a function to invoke as an argument, and wraps it. When `callbackPass()` |
| is used (or any other function that increments an internal callback counter), |
| the testing infrastructure keeps track of that callback and waits for all |
| outstanding callbacks to be called before automatically continuing to the next |
| test. A version of this test using `callbackPass()` may look like this. |
| |
| ```js |
| chrome.test.runTests([ |
| function step1() { |
| chrome.tabs.create({url: 'http://example.com'}, callbackPass(() => { |
| <verify state> |
| })); |
| chrome.storage.local.set({foo: 'bar'}, callbackPass(() => { |
| <verify state> |
| })); |
| }, |
| function step2() { |
| ... |
| } |
| ]); |
| ``` |
| |
| Now, `chrome.test.succeed()` will be internally called by the testing |
| infrastructure once both callbacks have been invoked. |
| |
| `callbackFail()` behaves similarly to `callbackPass()`, except that it |
| predominantly takes an expected error message that will be set in |
| `chrome.runtime.lastError` (though it takes a secondary argument of an optional |
| function to invoke). |
| |
| #### Advantages |
| The most obvious advantage to using `callbackPass()` and `callbackFail()` is |
| that it eliminates the need for more complex callback management. In addition |
| to keeping track of the callbacks, callbackPass() also automatically checks |
| that there was no API error raised in the callback, obviating the need for a |
| call to `chrome.test.assertNoLastError()`. This can lead to more succinct |
| code. |
| |
| #### Disadvantages |
| `callbackPass()` and `callbackFail()` can both lead to much less readable and |
| obvious code. Extension APIs (and JavaScript in general) are already heavily |
| asynchronous, and use of `callbackPass()` and `callbackFail()` can lead to even |
| more confusion about when a test will finish, or which order items will be |
| executed in. Coupled with the internal callback counter, hazards around |
| multiple calls to `chrome.test.succeed()` or `chrome.test.fail()`, and others, |
| and it is often much more clear to avoid using `callbackPass()` and |
| `callbackFail()`. By comparison, having a single call to |
| `chrome.test.succeed()` in an extension test function makes it more clear when |
| success is met, and what the final operation is. Additionally, there's no good |
| way to sequence multiple operations - there is only a single internal "callback |
| counter", which all callback-based utility methods share. Finally, having |
| multiple ways to signal a test is "done" leads to frequent developer and |
| reviewer confusion and misuse (e.g., calling `chrome.test.succeed()` within a |
| `callbackPass()` - see the [Do's And Don't's]). |
| |
| #### Alternatives |
| ##### Restructure the Test |
| In the example above, it's unclear why the `storage.set()` call and |
| `tabs.create()` call need to be within the same test. They could instead be |
| two separate test functions, passed serially in the array to |
| `chrome.test.runTests()`. |
| |
| ```js |
| chrome.test.runTests([ |
| function createTab() { |
| chrome.tabs.create({url: 'http://example.com'}, () => { |
| <verify state> |
| chrome.test.succeed(); |
| }); |
| }, |
| function initializeStorage() { |
| chrome.storage.local.set({foo: 'bar'}, () => { |
| <verify state> |
| chrome.test.succeed(); |
| }); |
| }, |
| function nextStep() { |
| ... |
| } |
| ]); |
| ``` |
| |
| ##### Use Promises |
| The `chrome.test` callback mechanism was created before [Promises] were |
| available as a language feature in JavaScript. Promises can provide similar |
| guarantees while providing more explicit direction to the reader about when a |
| test will finish. |
| |
| ```js |
| chrome.test.runTests([ |
| function step1() { |
| let tabPromise = new Promise((resolve) => { |
| chrome.tabs.create({url: 'http://example.com'}, () => { |
| <verify state> |
| resolve(); |
| }); |
| }); |
| let storagePromise = new Promise((resolve) => { |
| chrome.storage.local.set({foo: 'bar'}, () => { |
| <verify state> |
| resolve(); |
| }); |
| }); |
| Promise.all([tabPromise, storagePromise]).then(() => { |
| chrome.test.succeed(); |
| }); |
| }, |
| function step2() { |
| ... |
| } |
| ]); |
| ``` |
| |
| This solution is somewhat more verbose, but makes it more clear on which |
| criteria the test is waiting, and can allow for different "sequences" of |
| waiting. This will also be much more streamlined when test APIs themselves can |
| return promises. |
| |
| ##### Use Async Functions |
| The `chrome.test.runTests()` allows for [asynchronous functions] and using the |
| [await] keyword. In conjunction with the Extension system's asynchronous APIs, |
| this can lead to reasonably concise and readable code, such as the example |
| below: |
| |
| ```js |
| const tab = |
| await new Promise(resolve => chrome.tabs.create({url: url}, resolve)); |
| <verify state> |
| ``` |
| |
| For Promise-based calls in MV3 tests this can be simplified even more: |
| |
| ```js |
| const tab = await chrome.tabs.create({url: url}); |
| <verify state> |
| ``` |
| |
| ### listenOnce() and listenForever() |
| `chrome.test.listenOnce()` and `chrome.test.listenForever()` are utility |
| functions used when waiting on different events. Like `callbackPass()` and |
| `callbackFail()` above, they use the test API's internal callback counter, |
| allowing the test to finish automatically once all callbacks have been invoked. |
| Naturally, they share all the same disadvantages as well. |
| |
| `listenOnce()` waits for the event to be invoked a single time, and then |
| removes the listener and reduces the internal callback counter. Calling |
| `listenForever()` adds the listener and returns a function to be invoked at any |
| point; invoking this function will remove the listener and decrement the |
| callback counter. |
| |
| ### getConfig() |
| `chrome.test.getConfig()` retrieves the current configuration of the test |
| setup. This may be necessary for a variety of reasons - a common one is |
| needing the port on which the test server is running in order to construct |
| URLs, as below. |
| |
| ```js |
| chrome.test.getConfig((config) => { |
| let url = `http://example.com:${config.testServer.port}/simple.html`; |
| createTab(url); |
| }); |
| ``` |
| |
| ### sendMessage() |
| `chrome.test.sendMessage()` is used to communicate with the C++ side of the |
| browser test, allowing us to force synchronicity if necessary. It should be |
| used with [ExtensionTestMessageListener] on the C++ side. |
| |
| ```c++ |
| // test.cc: |
| IN_PROC_BROWSER_TEST_F(...) { |
| LoadExtension(...); |
| GURL url = GetASpecialURL(); |
| ExtensionTestMessageListener listener("clicked", ReplyBehavior::kWillReply); |
| ClickAction(); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| listener.Reply(url.spec()); |
| ... |
| } |
| ``` |
| |
| ```js |
| // extension_script.js: |
| chrome.action.onClicked.addListener(() => { |
| chrome.test.sendMessage('clicked', (specialUrl) => { |
| useSpecialUrl(specialUrl); |
| }); |
| }); |
| ``` |
| |
| ## Do's and Don't's |
| ### **Do** Write Small, "B[i|y]te-Size" Tests |
| One of the advantages of having the `chrome.test.runTests()` method and |
| infrastructure is that we can write small, unit-test style tests without |
| needing to pay the cost of an extra browser test execution (expensive) for |
| each individual test case. Because of this, we can write very targeted, |
| easy-to-consume, understandable test cases, rather than a single behemoth test |
| case. |
| |
| ### **Do** Prefer More Specific Asserts |
| `assertTrue()` and `assertFalse()` provide the least information in the logs |
| ("expected true, found false"-style messages). Assert-style methods like |
| `assertEq()`, `assertLastError()`, and `assertNoLastError()` can provide much |
| more detail (such as the actual value for `assertEq()`, or the error for |
| `assertNoLastError()`). |
| |
| ### **Do** Use Modern(ish) JS |
| Many tests were written years and years ago. Modern JS practices, such as |
| `let` and `const`, [arrow functions], [template literals], and more, generally |
| increase readability. Don't feel shy about using them just because other |
| examples don't. (The caveat to this is that these tests should not use |
| anything that isn't [approved](/styleguide/web/es.md) for Chromium JS use, |
| unless the use of it is explicitly necessary for the test.) |
| |
| ### **Don't** Mix chrome.test.notifyPass() and chrome.test.runTests() |
| `chrome.test.notifyPass()` (or `chrome.test.notifyFail()`) will finish the |
| entire suite. It should not be used with `chrome.test.runTests()`. Instead, |
| use `chrome.test.succeed()`. |
| |
| ### **Don't** Mix chrome.test.callbackPass() et al. and chrome.test.succeed() |
| If the callback counter is incremented anywhere in a test function, the test |
| infrastructure will automatically invoke `chrome.test.succeed()` when the |
| counter reaches zero. Calling `chrome.test.succeed()` in addition to |
| `callbackPass()`, `callbackFail()`, `listenOnce()`, or `listenForever()` can |
| result in unpredictable behavior, or masking failures. |
| |
| ### **Don't** Call other asynchronous functions after callbackPass/Fail() |
| Consider the following code: |
| |
| ```js |
| chrome.foo.asyncFunction(..., callbackPass(() => { |
| chrome.foo.asyncFunction2(..., () => { |
| // Extra stuff |
| }); |
| }); |
| ``` |
| |
| In this case, the callback counter will wait for the call to `asyncFunction()` |
| to complete and execute the inner function, and will then immediately end the |
| test case. The callback from `asyncFunction2()` will be invoked after the test |
| case is over, which can lead to flakiness or false-negatives. |
| |
| If you have to have nested functions, each should use `callbackPass()` or |
| `callbackFail()`. Better yet, migrate away from `callbackPass()` and |
| `callbackFail()` with [these alternatives](#Alternatives). |
| |
| ### **Don't** Have multiple "win" conditions |
| While it's possible to have multiple `notifyPass()` calls in a single C++ |
| browser test (by using multiple calls to `RunExtensionTest()` or using multiple |
| `ResultCatchers`), and by extension possible to mix `runTests()` with |
| `notifyPass()` with `sendMessage()` with everything else, it's generally a bad |
| idea. It makes the flow of control incredibly difficult to parse for author, |
| reviewer, and future reader, and frequently leads to subtle bugs being missed. |
| |
| Instead, if a complex chain of steps is needed, use either |
| `chrome.test.runTests()` or `chrome.test.sendMessage()` (if C++ coordination is |
| needed). |
| |
| ### **Don't** Go overboard with runTests() |
| It can be tempting to set up a 20-step sequence in |
| `chrome.test.runTests()`, but this makes it very difficult to understand the |
| total flow, harder to debug, and increases the risk of timing out (from simply |
| doing too much). If a test relies on multiple steps in `runTests()`, have a |
| dedicated browser test for those related steps, and restrict them to a |
| reasonable number. If a test has a series of unrelated steps, keep them small |
| and targeted (in good unit testing behavior). |
| |
| [writing extension tests]: extension_tests.md |
| [Do's And Don't's]: #Dos-and-Dont_s |
| [Promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise |
| [asynchronous functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function |
| [await]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await |
| [ExtensionTestMessageListener]: ../test/extension_test_message_listener.h |
| [arrow functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions |
| [template literals]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals |