130
edits
(Removing the custom asyncTest function with the native add_task) |
(Typo) |
||
| (10 intermediate revisions by 3 users not shown) | |||
| Line 1: | Line 1: | ||
This article gives some suggestions about how to write [https://wiki.mozilla.org/DevTools/Hacking# | This article gives some suggestions about how to write [https://wiki.mozilla.org/DevTools/Hacking#DevTools_Mochitests devtools browser-chrome mochitests]. | ||
The following suggestions assume you want to write new tests for a given new feature or bug that you are working on. | The following suggestions assume you want to write new tests for a given new feature or bug that you are working on. | ||
If instead you are fixing a failing test then it should be easy enough to change the test's code according to what's in the file already. | If instead you are fixing a failing test then it should be easy enough to change the test's code according to what's in the file already. | ||
One of the first things to keep in mind when creating tests is that it's almost always a better idea to create a new test file rather than add new test cases to an existing one. | One of the first things to keep in mind when creating tests is that it's almost always a better idea to create a new test file rather than to add new test cases to an existing one. | ||
* This prevents test files from growing up to the point where they timeout for running too long (test systems may be under lot's of stress at time and run a lot slower than your regular local environment). | * This prevents test files from growing up to the point where they timeout for running too long (test systems may be under lot's of stress at time and run a lot slower than your regular local environment). | ||
* But this also helps with making tests more maintainable, with many small files, it's easier to track a problem rather than in one huge file. | * But this also helps with making tests more maintainable, with many small files, it's easier to track a problem rather than in one huge file. | ||
| Line 58: | Line 58: | ||
* Base URLs for support files: TEST_URL_ROOT. This avoids having to duplicate the http://example.com/browser/browser/devtools/styleinspector/ URL fragment in all tests, | * Base URLs for support files: TEST_URL_ROOT. This avoids having to duplicate the http://example.com/browser/browser/devtools/styleinspector/ URL fragment in all tests, | ||
* waitForExplicitFinish() is called in head.js once and for all. All tests are asynchronous, so there's no need to call it again in each and every test, | * waitForExplicitFinish() is called in head.js once and for all. All tests are asynchronous, so there's no need to call it again in each and every test, | ||
* auto-cleanup: the toolbox is closed automatically and all tabs are closed, | * auto-cleanup: the toolbox is closed automatically and all tabs are closed, | ||
* tab addTab(url) | * tab addTab(url) | ||
| Line 78: | Line 77: | ||
const TEST_URL = TEST_URL_ROOT + "doc_some_test_page.html"; | const TEST_URL = TEST_URL_ROOT + "doc_some_test_page.html"; | ||
add_task(function*() { | |||
yield addTab(TEST_URL_ROOT); | yield addTab(TEST_URL_ROOT); | ||
let {toolbox, inspector, view} = yield openRuleView(); | let {toolbox, inspector, view} = yield openRuleView(); | ||
| Line 93: | Line 92: | ||
/* ... do something ... this function can yield */ | /* ... do something ... this function can yield */ | ||
} | } | ||
== Shared head.js file == | |||
A [https://dxr.mozilla.org/mozilla-central/source/devtools/client/framework/test/shared-head.js shared-head.js file] has been introduced to avoid duplicating common DevTools tests code in various head.js files. | |||
It's important to know whether or not the shared.js in your test directory already imports shared-head.js (look for a <code>Services.scriptloader.loadSubScript</code> call), as common helpers in shared-head.js might be useful for your test. | |||
If you're planning to work on a lot of new tests, it might be worth the time actually importing shared-head.js in your head.js if it isn't here already, and avoid duplicating code. | |||
== E10S (Electrolysis) == | |||
E10S is the codename for Firefox multi-process, and what that means is that the process in which the test runs isn't the same as the one in which the test content page runs. | |||
Learn more about E10S [https://timtaubert.de/blog/2011/08/firefox-electrolysis-101/ here], [[Electrolysis|and here]], [https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/The_message_manager and here]. | |||
One of the direct consequences of E10S on tests is that you cannot retrieve and manipulate objects from the content page as you'd do without E10S. | |||
Well this isn't entirely true, because of [https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Cross_Process_Object_Wrappers CPOWs] (cross-process object wrappers). Using CPOWs, you somehow can access the page, get to DOM nodes, and read their attributes for instance, but a lot of other things you'd expect to work without E10S won't work exactly the same or at all, and using CPOWs is discouraged. '''In fact it's even forbidden in browser code, and only temporarily allowed in mochitests'''. | |||
So when creating a new test, if this test needs to access the content page in any way, you can use [https://developer.mozilla.org/en-US/docs/The_message_manager the message manager] to communicate to a script loaded in the content process to do things for you instead of accessing objects in the page directly. | |||
One simple way to do this is to use the helper <code>ContentTask.spawn()</code> (see [https://dxr.mozilla.org/mozilla-central/search?q=ContentTask.spawn%28+path%3Adevtools%2Fclient&redirect=false&case=false this dxr search] for a list of usages in devtools tests). | |||
Note that a lot of tests only need to access the devtools toolbox UI anyway and don't need to interact with the content process at all. Since the toolbox lives in the same process as the test, you won't need to use the message manager to access it. | |||
== Asynchronous tests == | == Asynchronous tests == | ||
| Line 102: | Line 123: | ||
* head.js already calls waitForExplicitFinish() so there's no need for your new test to do it too. | * head.js already calls waitForExplicitFinish() so there's no need for your new test to do it too. | ||
* Using add_task with a generator function means that you can yield calls to functions that return promises. It also means your main test function can be written | * Using add_task with a generator function means that you can yield calls to functions that return promises. It also means your main test function can be written to almost look like synchronous code, simply adding yield before calls to asynchronous functions. Here is, for example, a for loop: | ||
for (let | for (let test of testData) { | ||
yield testCompletion( | yield testCompletion(test, editor, view); | ||
} | } | ||
| Line 135: | Line 156: | ||
=== Callbacks and promises === | === Callbacks and promises === | ||
Avoid multiple nested callbacks or chained promises. They make it hard to read the code. | Avoid multiple nested callbacks or chained promises. They make it hard to read the code. | ||
Thanks to | Thanks to add_task, it's easy to write asynchronous code that looks like flat, synchronous, code. | ||
=== Clean up after yourself === | === Clean up after yourself === | ||
| Line 151: | Line 172: | ||
const TESTS = [ | const TESTS = [ | ||
{desc: "add a class", cssSelector: "#id1", | {desc: "add a class", cssSelector: "#id1", makeChanges: function*() {...}}, | ||
{desc: "change href", cssSelector: "a.the-link", | {desc: "change href", cssSelector: "a.the-link", makeChanges: function*() {...}}, | ||
... | ... | ||
]; | ]; | ||
add_task(function*() { | |||
yield addTab("..."); | yield addTab("..."); | ||
let {toolbox, inspector} = yield openInspector(); | let {toolbox, inspector} = yield openInspector(); | ||
for (let step of TESTS) { | for (let step of TESTS) { | ||
info("Testing step: " + step.desc); | info("Testing step: " + step.desc); | ||
yield selectNode( | yield selectNode(step.cssSelector, inspector); | ||
yield step.makeChanges(); | |||
} | } | ||
}); | }); | ||
As shown in this code example, you can add as many test cases as you want in the TESTS array and the actual test code will remain very short, and easy to understand and maintain (note that when looping through test arrays, it's always a good idea to add a "desc" property that will be used in an info() log output). | As shown in this code example, you can add as many test cases as you want in the TESTS array and the actual test code will remain very short, and easy to understand and maintain (note that when looping through test arrays, it's always a good idea to add a "desc" property that will be used in an info() log output). | ||
| Line 186: | Line 202: | ||
== Adding new helpers == | == Adding new helpers == | ||
In some | In some cases, you may want to extract some common code from your test to use it another another test. | ||
Here's how to create a helper file: | |||
* If this is very common code that all tests could use, then add it to <code>devtools/client/framework/test/shared-head.js</code>. | |||
* If this is common code specific to a given tool, then add it to the corresponding <code>head.js</code> file. | |||
* If it isn't common enough to live in <code>head.js</code>, then it may be a good idea to create a helper file to avoid duplication anyway. Here's how to create a helper file: | |||
** Create a new file in your test directory, the naming convention should be helper_<description_of_the_helper>.js | |||
** Add it to the browser.ini support-files section, making sure it is sorted alphabetically | |||
** Load the helper file in the tests | |||
** browser/devtools/markupview/test/head.js has a handy loadHelperScript(fileName) function that you can use. | |||
** The file will be loaded in the test global scope, so any global function or variables it defines will be available (just like head.js). | |||
** Use the special ESLint comment <code>/* import-globals-from helper_file.js */</code> to prevent ESLint errors for undefined variables. | |||
In all cases, new helper functions should be properly commented with an jsdoc comment block. | |||
edits