summaryrefslogtreecommitdiffstats
path: root/devtools/client/application
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/application')
-rw-r--r--devtools/client/application/README.md334
-rw-r--r--devtools/client/application/application.css36
-rw-r--r--devtools/client/application/index.html22
-rw-r--r--devtools/client/application/initializer.js150
-rw-r--r--devtools/client/application/moz.build13
-rw-r--r--devtools/client/application/panel.js46
-rw-r--r--devtools/client/application/src/actions/index.js12
-rw-r--r--devtools/client/application/src/actions/manifest.js50
-rw-r--r--devtools/client/application/src/actions/moz.build11
-rw-r--r--devtools/client/application/src/actions/page.js20
-rw-r--r--devtools/client/application/src/actions/ui.js20
-rw-r--r--devtools/client/application/src/actions/workers.js51
-rw-r--r--devtools/client/application/src/base.css83
-rw-r--r--devtools/client/application/src/components/App.css34
-rw-r--r--devtools/client/application/src/components/App.js46
-rw-r--r--devtools/client/application/src/components/manifest/Manifest.js136
-rw-r--r--devtools/client/application/src/components/manifest/ManifestColorItem.css29
-rw-r--r--devtools/client/application/src/components/manifest/ManifestColorItem.js57
-rw-r--r--devtools/client/application/src/components/manifest/ManifestEmpty.js81
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIconItem.css7
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIconItem.js98
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssue.css17
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssue.js72
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssueList.css15
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssueList.js68
-rw-r--r--devtools/client/application/src/components/manifest/ManifestItem.css28
-rw-r--r--devtools/client/application/src/components/manifest/ManifestItem.js50
-rw-r--r--devtools/client/application/src/components/manifest/ManifestJsonLink.css9
-rw-r--r--devtools/client/application/src/components/manifest/ManifestJsonLink.js67
-rw-r--r--devtools/client/application/src/components/manifest/ManifestLoader.css14
-rw-r--r--devtools/client/application/src/components/manifest/ManifestLoader.js108
-rw-r--r--devtools/client/application/src/components/manifest/ManifestPage.js76
-rw-r--r--devtools/client/application/src/components/manifest/ManifestSection.css25
-rw-r--r--devtools/client/application/src/components/manifest/ManifestSection.js44
-rw-r--r--devtools/client/application/src/components/manifest/ManifestUrlItem.css8
-rw-r--r--devtools/client/application/src/components/manifest/ManifestUrlItem.js39
-rw-r--r--devtools/client/application/src/components/manifest/moz.build18
-rw-r--r--devtools/client/application/src/components/moz.build14
-rw-r--r--devtools/client/application/src/components/routing/PageSwitcher.css45
-rw-r--r--devtools/client/application/src/components/routing/PageSwitcher.js59
-rw-r--r--devtools/client/application/src/components/routing/Sidebar.css33
-rw-r--r--devtools/client/application/src/components/routing/Sidebar.js70
-rw-r--r--devtools/client/application/src/components/routing/SidebarItem.css33
-rw-r--r--devtools/client/application/src/components/routing/SidebarItem.js95
-rw-r--r--devtools/client/application/src/components/routing/moz.build5
-rw-r--r--devtools/client/application/src/components/service-workers/Registration.css73
-rw-r--r--devtools/client/application/src/components/service-workers/Registration.js153
-rw-r--r--devtools/client/application/src/components/service-workers/RegistrationList.css54
-rw-r--r--devtools/client/application/src/components/service-workers/RegistrationList.js92
-rw-r--r--devtools/client/application/src/components/service-workers/RegistrationListEmpty.js122
-rw-r--r--devtools/client/application/src/components/service-workers/Worker.css75
-rw-r--r--devtools/client/application/src/components/service-workers/Worker.js225
-rw-r--r--devtools/client/application/src/components/service-workers/WorkersPage.js71
-rw-r--r--devtools/client/application/src/components/service-workers/moz.build11
-rw-r--r--devtools/client/application/src/components/ui/UIButton.css75
-rw-r--r--devtools/client/application/src/components/ui/UIButton.js41
-rw-r--r--devtools/client/application/src/components/ui/moz.build7
-rw-r--r--devtools/client/application/src/constants.js61
-rw-r--r--devtools/client/application/src/create-store.js50
-rw-r--r--devtools/client/application/src/middleware/event-telemetry.js37
-rw-r--r--devtools/client/application/src/middleware/moz.build7
-rw-r--r--devtools/client/application/src/modules/application-services.js85
-rw-r--r--devtools/client/application/src/modules/l10n.js12
-rw-r--r--devtools/client/application/src/modules/moz.build8
-rw-r--r--devtools/client/application/src/moz.build17
-rw-r--r--devtools/client/application/src/reducers/index.js28
-rw-r--r--devtools/client/application/src/reducers/manifest-state.js158
-rw-r--r--devtools/client/application/src/reducers/moz.build11
-rw-r--r--devtools/client/application/src/reducers/page-state.js39
-rw-r--r--devtools/client/application/src/reducers/ui-state.js30
-rw-r--r--devtools/client/application/src/reducers/workers-state.js65
-rw-r--r--devtools/client/application/src/types/index.js18
-rw-r--r--devtools/client/application/src/types/manifest.js89
-rw-r--r--devtools/client/application/src/types/moz.build10
-rw-r--r--devtools/client/application/src/types/routing.js16
-rw-r--r--devtools/client/application/src/types/service-workers.js35
-rw-r--r--devtools/client/application/test/browser/browser.toml150
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_debug-service-worker.js61
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_list-domain-workers.js70
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_list-multiple-workers-same-registration.js64
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_list-several-workers.js54
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_list-single-worker.js64
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_list-unicode.js47
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_list-workers-empty.js29
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_manifest-display.js158
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_manifest-load.js67
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_manifest-open-json.js67
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_manifest-reload.js51
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_open-links.js48
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_sidebar.js82
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_start-service-worker.js54
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_target-switching.js68
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_telemetry-debug-worker.js49
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_telemetry-select-page.js26
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_telemetry-start-worker.js45
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_telemetry-unregister-worker.js37
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_unregister-worker.js36
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_viewsource-service-worker.js50
-rw-r--r--devtools/client/application/test/browser/browser_application_panel_worker-states.js62
-rw-r--r--devtools/client/application/test/browser/head.js134
-rw-r--r--devtools/client/application/test/browser/resources/manifest/icon.svg4
-rw-r--r--devtools/client/application/test/browser/resources/manifest/load-fail.html9
-rw-r--r--devtools/client/application/test/browser/resources/manifest/load-no-manifest.html8
-rw-r--r--devtools/client/application/test/browser/resources/manifest/load-ok-icons.html9
-rw-r--r--devtools/client/application/test/browser/resources/manifest/load-ok-json-error.html10
-rw-r--r--devtools/client/application/test/browser/resources/manifest/load-ok-manifest-link.html9
-rw-r--r--devtools/client/application/test/browser/resources/manifest/load-ok-warnings.html10
-rw-r--r--devtools/client/application/test/browser/resources/manifest/load-ok.html9
-rw-r--r--devtools/client/application/test/browser/resources/manifest/manifest.json3
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/controlled-install-sw.js29
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/controlled-install.html27
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/debug-sw.js18
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/debug.html25
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/dynamic-registration.html19
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/empty-sw.js4
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/empty.html11
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/scope-page.html19
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/simple-unicode.html15
-rw-r--r--devtools/client/application/test/browser/resources/service-workers/simple.html32
-rw-r--r--devtools/client/application/test/node/.eslintrc.js10
-rw-r--r--devtools/client/application/test/node/actions/actions_application_panel-manifest.test.js83
-rw-r--r--devtools/client/application/test/node/babel.config.js8
-rw-r--r--devtools/client/application/test/node/components/__snapshots__/components_application_panel-App.test.js.snap12
-rw-r--r--devtools/client/application/test/node/components/components_application_panel-App.test.js26
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-Manifest.test.js.snap396
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestColorItem.test.js.snap58
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestEmpty.test.js.snap46
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIconItem.test.js.snap106
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIssue.test.js.snap49
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIssueList.test.js.snap89
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestItem.test.js.snap35
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestJsonLink.test.js.snap26
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestLoader.test.js.snap50
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestPage.test.js.snap80
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestSection.test.js.snap30
-rw-r--r--devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestUrlItem.test.js.snap21
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-Manifest.test.js73
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestColorItem.test.js48
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestEmpty.test.js23
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIconItem.test.js48
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIssue.test.js30
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIssueList.test.js59
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestItem.test.js28
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestJsonLink.test.js36
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestLoader.test.js82
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestPage.test.js59
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestSection.test.js34
-rw-r--r--devtools/client/application/test/node/components/manifest/components_application_panel-ManifestUrlItem.test.js30
-rw-r--r--devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-PageSwitcher.test.js.snap9
-rw-r--r--devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-Sidebar.test.js.snap64
-rw-r--r--devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-SidebarItem.test.js.snap141
-rw-r--r--devtools/client/application/test/node/components/routing/components_application_panel-PageSwitcher.test.js73
-rw-r--r--devtools/client/application/test/node/components/routing/components_application_panel-Sidebar.test.js51
-rw-r--r--devtools/client/application/test/node/components/routing/components_application_panel-SidebarItem.test.js82
-rw-r--r--devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-Registration.test.js.snap180
-rw-r--r--devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationList.test.js.snap159
-rw-r--r--devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationListEmpty.test.js.snap66
-rw-r--r--devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-Worker.test.js.snap132
-rw-r--r--devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-WorkersPage.test.js.snap143
-rw-r--r--devtools/client/application/test/node/components/service-workers/components_application_panel-Registration.test.js88
-rw-r--r--devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationList.test.js43
-rw-r--r--devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationListEmpty.test.js23
-rw-r--r--devtools/client/application/test/node/components/service-workers/components_application_panel-Worker.test.js110
-rw-r--r--devtools/client/application/test/node/components/service-workers/components_application_panel-WorkersPage.test.js82
-rw-r--r--devtools/client/application/test/node/fixtures/data/constants.js312
-rw-r--r--devtools/client/application/test/node/helpers.js31
-rw-r--r--devtools/client/application/test/node/jest.config.js14
-rw-r--r--devtools/client/application/test/node/package.json25
-rw-r--r--devtools/client/application/test/node/setup.js15
-rw-r--r--devtools/client/application/test/node/yarn.lock3563
-rw-r--r--devtools/client/application/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/application/test/xpcshell/test_manifest_reducer.js201
-rw-r--r--devtools/client/application/test/xpcshell/test_page_reducer.js22
-rw-r--r--devtools/client/application/test/xpcshell/test_ui_reducer.js22
-rw-r--r--devtools/client/application/test/xpcshell/test_workers_reducer.js115
-rw-r--r--devtools/client/application/test/xpcshell/xpcshell-head.js10
-rw-r--r--devtools/client/application/test/xpcshell/xpcshell.toml13
177 files changed, 13436 insertions, 0 deletions
diff --git a/devtools/client/application/README.md b/devtools/client/application/README.md
new file mode 100644
index 0000000000..c7abb926f3
--- /dev/null
+++ b/devtools/client/application/README.md
@@ -0,0 +1,334 @@
+# Application
+
+## About the Application panel
+
+The Application panel is a Firefox Developer Tools panel meant to allow the inspection and debugging of features usually present in Progressive Web Apps, such as service workers or the Web App Manifest.
+
+## Technical overview
+
+The Application panel is a React + Redux web app.
+
+### Source directory structure
+
+The application panel lives in the `devtools/client/application` directory. Inside this root directory, the most relevant subdirectories and files are:
+
+```
+.
+├── application.css -> root CSS stylesheet
+├── initializer.js -> panel initialization
+├── src
+│ ├── actions -> Redux actions
+│ ├── components -> React components
+│ ├── modules -> Business logic and helpers
+│ └── reducers -> Redux reducers and states
+└── test
+ ├── browser -> mochitests (e2e/integration)
+ ├── node -> Jest unit tests
+ └── xpcshell -> xpcshell unit tests
+```
+
+### Panel registration
+
+The panel is defined in `devtools/client/application/panel.js` – which in turn calls the `.bootstrap()` method defined in `devtools/client/application/initializer.js`.
+
+The panel is registered along the rest of the Developer Tools panels, in `devtools/client/definitions.js`.
+
+### Localization
+
+The panel uses the [fluent-react](https://github.com/projectfluent/fluent.js/wiki/React-Bindings) library. The localization file is located at `devtools/client/locales/en-US/application.ftl` and it follows [Fluent syntax](https://projectfluent.org/fluent/guide/).
+
+You should read the [Fluent for Firefox developers](https://firefox-source-docs.mozilla.org/l10n/fluent/tutorial.html) and [Guidelines for Fluent Reviewers](https://firefox-source-docs.mozilla.org/intl/l10n/l10n/fluent_review.html) guides.
+
+#### Adding a new string
+
+If you need to localize a text, add a new message ID to the localization file (`devtoos/client/en-US/application.ftl`). Then you can use this ID as a prop for Fluent's `<Localized>` component or `getString()` function.
+
+When using ID's from the React code, always write the full ID instead of building it with concatenation, interpolation, etc. (this makes localing ID's easier afterwards). For instance, let's say you have two ID's, `error-message` and `warning-message`, and you need to use one or the other depending on a condition.
+
+✅ **Do:**
+
+```js
+const localizationIds = {
+ error: "error-message",
+ warning: "warning-message",
+};
+
+const id = localizationIds[messageLevel];
+```
+
+❌ **Don't:**
+
+```js
+const id = `${messageLevel}-message`;
+```
+
+#### Updating an existing string
+
+If you need to modify an existing string, you need to create a new ID and don't remove the previous one. For instance, if we had this string:
+
+```
+some-localized-string = This will be modified later
+```
+
+And we need to update the content, we would create a new ID and remove the previous one intact:
+
+```diff
+- some-localized-string = This will be modified later
++ some-localized-string2 = This is the updated string
+```
+
+Within the React component code, you would use the newly created ID.
+
+```js
+const localizedText = l10n.getString("some-localized-string2");
+```
+
+### React components
+
+Components are located in the `devtools/client/application/src/components` directory.
+
+Each component has a single `.js` file, plus an optional `.css` file, so each component is responsible of handling their own styles.
+
+- The `App` component is the root component of the Application panel.
+- In `components/service-workers` are the components that render all of the UI related to inspect and debug service workers.
+- In `components/manifest` are the components that render everything related to inspecting a Web App Manifest.
+- In the `components/routing` directory there are components related to switching from one section of the panel to the other, like a sidebar or a page switcher container.
+- Inside the `components/ui` directory there are generic UI components that can be reused from other parts of the panel.
+
+### Redux
+
+The Application panel uses Redux to handle app state and communication between components.
+
+#### Substates
+
+The Redux state is set up in `devtools/client/application/src/create-store.js`. The state contains the following substates:
+
+- `workers`: contains data related to inspecting and debugging service workers.
+- `manifest`: contains state related to inspecting the Web App Manifest.
+- `page`: contains general data related to the web page that is being inspected.
+- `ui`: contains data related to the UI that don't really fit any other state.
+
+#### Actions
+
+Synchronous actions are regular Redux actions. Their type is defined in the general constants file (`devtools/client/application/src/constants.js`).
+
+**Asynchronous** actions are achieved thanks to the **thunk middleware**. They follow a three-action pattern `*_START`, `*_SUCCESS`, `*_FAILURE`. For instance:
+
+```js
+function fooAction() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: FOO_START });
+
+ try {
+ const result = await foo();
+ dispatch({ type: FOO_SUCCESS });
+ }
+ catch (err) {
+ console.error(err);
+ dispatch({ type: FOO_FAILURE });
+ }
+ }
+}
+```
+
+#### Middleware
+
+We are using the following middlewares with Redux:
+
+- [redux-thunk](https://github.com/reduxjs/redux-thunk): This is a shared middleware used by other DevTools modules that allows to dispatch asynchronous actions.
+
+### Constants
+
+We are sharing some constants / enum-like constants across the source. They are located in `devtools/client/application/src/constants.js`. For values that are not shared, they should stay within their relevant scope/file.
+
+## Tests
+
+You should read DevTools' general [guidelines for automated tests](https://firefox-source-docs.mozilla.org/devtools/tests/README.html).
+
+### Mochitests (e2e / integration)
+
+End to end and integration tests are made with **Mochitests**. These tests open a browser and will load an HTML and open the toolbox with the Application panel.
+
+These tests are located in `devtools/client/application/test/browser`.
+
+Besides the usual DevTools test helpers, the Application panel adds some other helper functions in `devtools/client/application/test/browser/head.js`
+
+### Unit tests with xpcshell
+
+We are using xpcshell unit tests for the **Redux reducers** in `devtools/client/application/test/xpcshell`.
+
+Other unit tests that don't need Enzyme or other npm modules should be added here too.
+
+### Unit tests with Jest
+
+We are using Jest with Enzyme to test **React components** and **Redux actions** in `devtools/client/application/test/node`. Some considerations:
+
+- There are some **stubs/mocks** for some libraries and modules. These are placed in `devtools/client/application/test/node/fixtures`.
+- The **localization system is mocked**. For `<Localized>` components, the Enzyme snapshots will reflect the localization ID. Ex: `<Localized id="some-localization">`. The actual localized string will not be rendered with `.shallow()`. Calls to `getstring()` will return the string ID that we pass as an argument.
+- The **redux store is mocked** as well. To create a fake store to test a component that uses `connect()` to fetch data from the store, use the helper `setupStore()` included in `devtools/client/application/test/node/helpers.js`.
+
+#### Snapshots
+
+Most Jest tests will involve a snapshot matching expectation.
+
+**Snapshots should be produced with [`shallow()`](https://airbnb.io/enzyme/docs/api/shallow.html)** and _not_ `mount()`, to ensure that we are only testing the current component.
+
+**Components that are wrapped** with `connect()` or other wrapper components, need their snapshot to be rendered with [`dive()`](https://airbnb.io/enzyme/docs/api/ShallowWrapper/dive.html) (one call to `dive()` per wrapper component).
+
+## CSS
+
+We are using a BEM-like style for the following reasons:
+
+- To avoid collision between the styles of components.
+- To have a consistent methodology on how to use CSS.
+- To ensure we have rules with low specificity and thus styles are easier to override and maintain.
+
+### Stylesheet organization
+
+#### File structure
+
+**Each component has its own CSS stylesheet**, in the same directory and with the same name as the JavaScript file, only with a `.css` extension. So `Foo.js` would have a `Foo.css` stylesheet.
+
+These stylesheets are imported from `devtools/client/application/application.css` in alphabetical order –BEM-like naming and low specificity, when done right, should ensure that styles work regardless of the order the stylesheets are included.
+
+#### Base styles
+
+Another important stylesheet is `devtools/client/application/base.css`, which is the one that is imported first from `application.css`, regardless the alphabetical order. In this stylesheet we do:
+
+- Variables definitions
+- Utility classes
+- Some styles that are applied to the whole panel, like some resets or default values.
+
+#### External stylesheets imported in the panel
+
+Besides the usual user-agent styles, all DevTools panels automatically import the stylesheets located at `devtools/client/themes/`. The most relevant for the Application panel are:
+
+- `devtools/client/themes/variables.css`: variables are defined here
+- `devtools/client/themes/common.css`: rules that are applied to every panel
+
+In the Application panel, we are not using some of the classes and rules defined in `common.css`, since they don't follow the BEM convention and some of them have high specificity. However, there are rules defined at the element level and this will get applied to the markup we use.
+
+From `variables.css`, at the moment we mainly use color names. See the section below for details. Other variables available there that contain values for common elements (like heights, widths, etc.) should also be used when we implement those components.
+
+### Colors and themes
+
+All DevTools panels should support both **light and dark themes**. The active theme can be changed by the user at DevTools Settings.
+
+The way this is handled is that a `.theme-light` or `.theme-dark` class is automatically added to the root `<html>` element of each panel. Then, in `variables.css`, there are some CSS variables with different values depending on which of those classes the root element has.
+
+- We should use the colors defined in `variables.css`.
+- If the other panels are using a particular color for an element we also have, we should re-use the same color.
+- If needed, we can create a CSS variable in `base.css` that takes the value of another color variable defined in `variables.css`
+- If the desired color is not available in `variables.css`, we can create a new one, but we have to define values for both light and dark themes.
+
+#### Writing CSS selectors
+
+As mentioned earlier, we are using a system to create rules similar to [BEM](http://getbem.com/naming/), although not strictly identical.
+
+#### Low specificity rules
+
+Rules should have **the lowest specifity** that is possible. In practice, this means that we should try to use a **single class selector** for writing the rule. Ex:
+
+```css
+.manifest-item {
+ /* ... */
+}
+```
+
+Note that **pseudoclasses are pseudoselectors are OK** to use.
+
+#### Hierarchy
+
+When an element is semantically the child of another element, we should also use a single class selector. In BEM, this is achieved by separating potential class names with a double underscore `__`, so we have a single classname.
+
+✅ **Do:**
+
+```css
+.manifest-item { /* ... */ }
+.manifest-item__value { /* ... */ }
+```
+
+❌ **Don't:**
+
+```css
+.manifest-item { /* ... */ }
+.manifest-item .value { /* ... */ }
+```
+
+We should not go deeper than one level of `__`. We don't have to replicate the whole DOM structure:
+
+- If the style rule should be independent of the parent, it does not need to use an `__`.
+- If the style rule does not make sense on its own, and it will be scoped to a particular section of the DOM tree, don't replicate the whole DOM structure and use the top-level ancestor class name instead.
+
+For instance, if we have the following HTML markup and we have to choose a class name for the `<dt>` element:
+
+```html
+<li class="worker">
+ <dl class="worker__data">
+ <dt class="..."></dt>
+ <!-- ... -->
+```
+
+✅ **Do:**
+
+```css
+.worker__meta-name { /* ... */ }
+```
+
+❌ **Don't:**
+
+```css
+.worker__data__meta-name { /* ... */ }
+```
+
+#### Style variations
+
+Sometimes we have a component that should change its style depending on its state, or we have some rules that allow us to customize further how something looks. For instance, we could have a generic rule for buttons, and then some extra rules for primary buttons, or for tiny ones.
+
+In BEM, style variants are created with a double hyphen `--`. For example:
+
+```css
+.ui-button { /* styles for buttons */ }
+.ui-button--micro { /* specific styles for tiny buttons */ }
+```
+
+Then, in the component, _both_ classes are to be used.
+
+```html
+<button class="ui-button ui-button--micro">
+```
+
+#### Base size
+
+Photon has a base unit of `4px` for sizes, paddings, margins… This value is stored in the CSS variable `--base-unit`.
+
+Our sizes and other dimensions should be defined based on this variable. Since we are using raw CSS and not a pre/post processor, we need to use `calc()` to achieve this.
+
+✅ **Do:**
+
+```css
+.ui-button {
+ height: calc(var(--base-unit) * 6);
+ /* ... */
+}
+```
+
+❌ **Don't:**
+
+```css
+.ui-button {
+ height: 24px;
+ /* ... */
+}
+```
+
+---
+
+## Contact
+
+If you have questions about the code, features, planning, etc. the current active team is:
+
+- Product management: Harald Kischner (`:digitarald`)
+- Engineering management: Patrick Brosset (`:pbro`)
+- Engineering: Belén Albeza (`:ladybenko`)
+- Engineering: Ola Gasidlo (`:ola`)
diff --git a/devtools/client/application/application.css b/devtools/client/application/application.css
new file mode 100644
index 0000000000..61b616faec
--- /dev/null
+++ b/devtools/client/application/application.css
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /*
+* Global styles
+*/
+@import "chrome://devtools/content/application/src/base.css";
+
+
+/*
+* Components
+*/
+@import "chrome://devtools/content/application/src/components/App.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestColorItem.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestIconItem.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestIssue.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestIssueList.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestItem.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestJsonLink.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestLoader.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestSection.css";
+@import "chrome://devtools/content/application/src/components/manifest/ManifestUrlItem.css";
+@import "chrome://devtools/content/application/src/components/routing/PageSwitcher.css";
+@import "chrome://devtools/content/application/src/components/routing/Sidebar.css";
+@import "chrome://devtools/content/application/src/components/routing/SidebarItem.css";
+@import "chrome://devtools/content/application/src/components/service-workers/Registration.css";
+@import "chrome://devtools/content/application/src/components/service-workers/RegistrationList.css";
+@import "chrome://devtools/content/application/src/components/service-workers/Worker.css";
+@import "chrome://devtools/content/application/src/components/ui/UIButton.css";
+
+html,
+body,
+#mount {
+ height: 100vh;
+}
diff --git a/devtools/client/application/index.html b/devtools/client/application/index.html
new file mode 100644
index 0000000000..0b9bd03512
--- /dev/null
+++ b/devtools/client/application/index.html
@@ -0,0 +1,22 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html dir="">
+ <head>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome: resource:; img-src http: https: data: chrome:;"
+ />
+ <link
+ rel="stylesheet"
+ type="text/css"
+ href="chrome://devtools/content/application/application.css"
+ />
+ </head>
+ <body class="theme-body" role="application">
+ <div id="mount"></div>
+ <script src="chrome://devtools/content/shared/theme-switching.js"></script>
+ <script src="resource://devtools/client/application/initializer.js"></script>
+ </body>
+</html>
diff --git a/devtools/client/application/initializer.js b/devtools/client/application/initializer.js
new file mode 100644
index 0000000000..fbfcbc1edc
--- /dev/null
+++ b/devtools/client/application/initializer.js
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+);
+const require = BrowserLoader({
+ baseURI: "resource://devtools/client/application/",
+ window,
+}).require;
+
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ render,
+ unmountComponentAtNode,
+} = require("resource://devtools/client/shared/vendor/react-dom.js");
+const Provider = createFactory(
+ require("resource://devtools/client/shared/vendor/react-redux.js").Provider
+);
+const {
+ bindActionCreators,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const {
+ configureStore,
+} = require("resource://devtools/client/application/src/create-store.js");
+const actions = require("resource://devtools/client/application/src/actions/index.js");
+
+const {
+ WorkersListener,
+} = require("resource://devtools/client/shared/workers-listener.js");
+
+const {
+ services,
+} = require("resource://devtools/client/application/src/modules/application-services.js");
+
+const App = createFactory(
+ require("resource://devtools/client/application/src/components/App.js")
+);
+
+const {
+ safeAsyncMethod,
+} = require("resource://devtools/shared/async-utils.js");
+
+/**
+ * Global Application object in this panel. This object is expected by panel.js and is
+ * called to start the UI for the panel.
+ */
+window.Application = {
+ async bootstrap({ toolbox, commands, panel }) {
+ // bind event handlers to `this`
+ this.updateDomain = this.updateDomain.bind(this);
+
+ // wrap updateWorkers to swallow rejections occurring after destroy
+ this.safeUpdateWorkers = safeAsyncMethod(
+ () => this.updateWorkers(),
+ () => this._destroyed
+ );
+
+ this.toolbox = toolbox;
+ this._commands = commands;
+ this.client = commands.client;
+
+ this.store = configureStore(toolbox.telemetry);
+ this.actions = bindActionCreators(actions, this.store.dispatch);
+
+ services.init(this.toolbox);
+ await l10n.init(["devtools/client/application.ftl"]);
+
+ await this.updateWorkers();
+ this.workersListener = new WorkersListener(this.client.mainRoot);
+ this.workersListener.addListener(this.safeUpdateWorkers);
+
+ const deviceFront = await this.client.mainRoot.getFront("device");
+ const { canDebugServiceWorkers } = await deviceFront.getDescription();
+ this.actions.updateCanDebugWorkers(
+ canDebugServiceWorkers && services.features.doesDebuggerSupportWorkers
+ );
+
+ this.onResourceAvailable = this.onResourceAvailable.bind(this);
+ await this._commands.resourceCommand.watchResources(
+ [this._commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this.onResourceAvailable,
+ }
+ );
+
+ // Render the root Application component.
+ this.mount = document.querySelector("#mount");
+ const app = App({
+ client: this.client,
+ fluentBundles: l10n.getBundles(),
+ });
+ render(Provider({ store: this.store }, app), this.mount);
+ },
+
+ async updateWorkers() {
+ const registrationsWithWorkers =
+ await this.client.mainRoot.listAllServiceWorkers();
+ this.actions.updateWorkers(registrationsWithWorkers);
+ },
+
+ updateDomain() {
+ this.actions.updateDomain(this.toolbox.target.url);
+ },
+
+ handleOnNavigate() {
+ this.updateDomain();
+ this.actions.resetManifest();
+ },
+
+ onResourceAvailable(resources) {
+ // Only consider top level document, and ignore remote iframes top document
+ const hasDocumentDomComplete = resources.some(
+ resource =>
+ resource.resourceType ===
+ this._commands.resourceCommand.TYPES.DOCUMENT_EVENT &&
+ resource.name === "dom-complete" &&
+ resource.targetFront.isTopLevel
+ );
+ if (hasDocumentDomComplete) {
+ this.handleOnNavigate(); // update domain and manifest for the new target
+ }
+ },
+
+ destroy() {
+ this.workersListener.removeListener();
+
+ this._commands.resourceCommand.unwatchResources(
+ [this._commands.resourceCommand.TYPES.DOCUMENT_EVENT],
+ { onAvailable: this.onResourceAvailable }
+ );
+
+ unmountComponentAtNode(this.mount);
+ this.mount = null;
+ this.toolbox = null;
+ this.client = null;
+ this._commands = null;
+ this.workersListener = null;
+ this._destroyed = true;
+ },
+};
diff --git a/devtools/client/application/moz.build b/devtools/client/application/moz.build
new file mode 100644
index 0000000000..f66b62ae39
--- /dev/null
+++ b/devtools/client/application/moz.build
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "src",
+]
+
+DevToolsModules("initializer.js", "panel.js")
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
diff --git a/devtools/client/application/panel.js b/devtools/client/application/panel.js
new file mode 100644
index 0000000000..58316c53cd
--- /dev/null
+++ b/devtools/client/application/panel.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * DevTools panel responsible for the application tool, which lists and allows to debug
+ * service workers.
+ */
+class ApplicationPanel {
+ /**
+ * Constructor.
+ *
+ * @param {Window} panelWin
+ * The frame/window dedicated to this panel.
+ * @param {Toolbox} toolbox
+ * The toolbox instance responsible for this panel.
+ * @param {Object} commands
+ * The commands object with all interfaces defined from devtools/shared/commands/
+ */
+ constructor(panelWin, toolbox, commands) {
+ this.panelWin = panelWin;
+ this.toolbox = toolbox;
+ this.commands = commands;
+ }
+
+ async open() {
+ await this.panelWin.Application.bootstrap({
+ toolbox: this.toolbox,
+ commands: this.commands,
+ panel: this,
+ });
+
+ return this;
+ }
+
+ destroy() {
+ this.panelWin.Application.destroy();
+ this.panelWin = null;
+ this.toolbox = null;
+ this.emit("destroyed");
+ }
+}
+
+exports.ApplicationPanel = ApplicationPanel;
diff --git a/devtools/client/application/src/actions/index.js b/devtools/client/application/src/actions/index.js
new file mode 100644
index 0000000000..67e9cbfd88
--- /dev/null
+++ b/devtools/client/application/src/actions/index.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const workers = require("resource://devtools/client/application/src/actions/workers.js");
+const page = require("resource://devtools/client/application/src/actions/page.js");
+const ui = require("resource://devtools/client/application/src/actions/ui.js");
+const manifest = require("resource://devtools/client/application/src/actions/manifest.js");
+
+Object.assign(exports, workers, page, ui, manifest);
diff --git a/devtools/client/application/src/actions/manifest.js b/devtools/client/application/src/actions/manifest.js
new file mode 100644
index 0000000000..050fab2b89
--- /dev/null
+++ b/devtools/client/application/src/actions/manifest.js
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const {
+ services,
+ ManifestDevToolsError,
+} = require("resource://devtools/client/application/src/modules/application-services.js");
+const {
+ FETCH_MANIFEST_FAILURE,
+ FETCH_MANIFEST_START,
+ FETCH_MANIFEST_SUCCESS,
+ RESET_MANIFEST,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function fetchManifest() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: FETCH_MANIFEST_START });
+ try {
+ const manifest = await services.fetchManifest();
+ dispatch({ type: FETCH_MANIFEST_SUCCESS, manifest });
+ } catch (error) {
+ let errorMessage = error.message;
+
+ // since Firefox DevTools errors may not make sense for the user, swap
+ // their message for a generic one.
+ if (error instanceof ManifestDevToolsError) {
+ console.error(error);
+ errorMessage = l10n.getString("manifest-loaded-devtools-error");
+ }
+
+ dispatch({ type: FETCH_MANIFEST_FAILURE, error: errorMessage });
+ }
+ };
+}
+
+function resetManifest() {
+ return { type: RESET_MANIFEST };
+}
+
+module.exports = {
+ fetchManifest,
+ resetManifest,
+};
diff --git a/devtools/client/application/src/actions/moz.build b/devtools/client/application/src/actions/moz.build
new file mode 100644
index 0000000000..f2a41f8674
--- /dev/null
+++ b/devtools/client/application/src/actions/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "index.js",
+ "manifest.js",
+ "page.js",
+ "ui.js",
+ "workers.js",
+)
diff --git a/devtools/client/application/src/actions/page.js b/devtools/client/application/src/actions/page.js
new file mode 100644
index 0000000000..1348a4aaa2
--- /dev/null
+++ b/devtools/client/application/src/actions/page.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ UPDATE_DOMAIN,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function updateDomain(url) {
+ return {
+ type: UPDATE_DOMAIN,
+ url,
+ };
+}
+
+module.exports = {
+ updateDomain,
+};
diff --git a/devtools/client/application/src/actions/ui.js b/devtools/client/application/src/actions/ui.js
new file mode 100644
index 0000000000..92de169ab0
--- /dev/null
+++ b/devtools/client/application/src/actions/ui.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ UPDATE_SELECTED_PAGE,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function updateSelectedPage(selectedPage) {
+ return {
+ type: UPDATE_SELECTED_PAGE,
+ selectedPage,
+ };
+}
+
+module.exports = {
+ updateSelectedPage,
+};
diff --git a/devtools/client/application/src/actions/workers.js b/devtools/client/application/src/actions/workers.js
new file mode 100644
index 0000000000..375dfe9ba7
--- /dev/null
+++ b/devtools/client/application/src/actions/workers.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ START_WORKER,
+ UNREGISTER_WORKER,
+ UPDATE_CAN_DEBUG_WORKERS,
+ UPDATE_WORKERS,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function startWorker(worker) {
+ const { registrationFront } = worker;
+ registrationFront.start();
+
+ return {
+ type: START_WORKER,
+ };
+}
+
+function unregisterWorker(registration) {
+ const { registrationFront } = registration;
+ registrationFront.unregister();
+
+ return {
+ type: UNREGISTER_WORKER,
+ };
+}
+
+function updateWorkers(workers) {
+ return {
+ type: UPDATE_WORKERS,
+ workers,
+ };
+}
+
+function updateCanDebugWorkers(canDebugWorkers) {
+ return {
+ type: UPDATE_CAN_DEBUG_WORKERS,
+ canDebugWorkers,
+ };
+}
+
+module.exports = {
+ startWorker,
+ unregisterWorker,
+ updateCanDebugWorkers,
+ updateWorkers,
+};
diff --git a/devtools/client/application/src/base.css b/devtools/client/application/src/base.css
new file mode 100644
index 0000000000..85b475a875
--- /dev/null
+++ b/devtools/client/application/src/base.css
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+:root {
+ /* Typography from Photon */
+ /* See https://firefox-dev.tools/photon/visuals/typography.html */
+ --caption-10-font-size: 11px;
+ --caption-10-font-weight: 400;
+ --body-10-font-size: 13px;
+ --body-10-font-weight: 400;
+ --body-20-font-size: 15px;
+ --body-20-font-weight: 400;
+ --body-20-font-weight-bold: 700;
+ --title-10-font-size: 13px;
+ --title-10-font-weight: 600;
+ --title-20-font-size: 17px;
+ --title-20-font-weight: 400;
+ --title-30-font-size: 22px;
+
+ /* Global styles */
+ --base-line-height: 1.8;
+ --list-line-height: 1.25;
+
+ /* Global colours */
+ --separator-color: var(--theme-splitter-color);
+ --bg-color: var(--theme-toolbar-background);
+ --highlight-color: var(--theme-toolbar-background-hover);
+
+ /* extra, raw colors */
+ --blue-50-a30: rgba(10, 132, 255, 0.3);
+
+ /* Global layout vars */
+ --base-unit: 4px;
+
+ /* these are the color for icons in empty pages - note that these are not
+ available in devtools' variables.css */
+ --dimmed-icon-color: #d3d3d3;
+}
+
+:root.theme-dark {
+ --dimmed-icon-color: #484848;
+}
+
+/*
+* Reset some tags
+*/
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ line-height: var(--base-line-height);
+}
+
+ul {
+ line-height: var(--list-line-height);
+}
+
+a {
+ color: var(--theme-link-color);
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+p {
+ margin: 0;
+}
+
+table {
+ border-spacing: 0;
+}
+
+/*
+ * utility classes
+ */
+
+.technical-text {
+ font-family: monospace;
+}
diff --git a/devtools/client/application/src/components/App.css b/devtools/client/application/src/components/App.css
new file mode 100644
index 0000000000..e0bcef7d2d
--- /dev/null
+++ b/devtools/client/application/src/components/App.css
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * General styles
+ */
+
+a.disabled-link,
+a.disabled-link:hover,
+a.disabled-link:visited {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.app {
+ display: grid;
+ min-width: calc(var(--base-unit) * 90);
+}
+
+/* wide layout -> two columns, 1 row */
+@media(min-width: 701px) {
+ .app {
+ grid-template-columns: calc(var(--base-unit) * 50) auto;
+ height: 100vh;
+ }
+}
+
+/* vertical layout -> 1 column, 2 rows */
+@media(max-width: 700px) {
+ .app {
+ grid-template-rows: auto 1fr;
+ }
+}
diff --git a/devtools/client/application/src/components/App.js b/devtools/client/application/src/components/App.js
new file mode 100644
index 0000000000..eac53e0cf6
--- /dev/null
+++ b/devtools/client/application/src/components/App.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ main,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
+
+const PageSwitcher = createFactory(
+ require("resource://devtools/client/application/src/components/routing/PageSwitcher.js")
+);
+const Sidebar = createFactory(
+ require("resource://devtools/client/application/src/components/routing/Sidebar.js")
+);
+
+/**
+ * This is the main component for the application panel.
+ */
+class App extends PureComponent {
+ static get propTypes() {
+ return {
+ fluentBundles: PropTypes.array.isRequired,
+ };
+ }
+
+ render() {
+ const { fluentBundles } = this.props;
+
+ return LocalizationProvider(
+ { bundles: fluentBundles },
+ main({ className: `app` }, Sidebar({}), PageSwitcher({}))
+ );
+ }
+}
+
+module.exports = App;
diff --git a/devtools/client/application/src/components/manifest/Manifest.js b/devtools/client/application/src/components/manifest/Manifest.js
new file mode 100644
index 0000000000..f6b5f19b37
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/Manifest.js
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ article,
+ h1,
+ table,
+ tbody,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+const {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const ManifestColorItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestColorItem.js")
+);
+const ManifestIconItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIconItem.js")
+);
+const ManifestUrlItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestUrlItem.js")
+);
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+const ManifestIssueList = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIssueList.js")
+);
+const ManifestSection = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestSection.js")
+);
+const ManifestJsonLink = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestJsonLink.js")
+);
+
+const {
+ MANIFEST_MEMBER_VALUE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+/**
+ * A canonical manifest, splitted in different sections
+ */
+class Manifest extends PureComponent {
+ static get propTypes() {
+ return {
+ ...Types.manifest, // { identity, presentation, icons, validation, url }
+ };
+ }
+
+ renderIssueSection() {
+ const { validation } = this.props;
+ const shouldRender = validation && !!validation.length;
+
+ return shouldRender
+ ? ManifestSection(
+ {
+ key: `manifest-section-0`,
+ title: l10n.getString("manifest-item-warnings"),
+ },
+ ManifestIssueList({ issues: validation })
+ )
+ : null;
+ }
+
+ renderMember({ key, value, type }, index) {
+ let domKey = key;
+ switch (type) {
+ case MANIFEST_MEMBER_VALUE_TYPES.COLOR:
+ return ManifestColorItem({ label: key, key: domKey, value });
+ case MANIFEST_MEMBER_VALUE_TYPES.ICON:
+ // since the manifest may have keys with empty size/contentType,
+ // we cannot use them as unique IDs
+ domKey = index;
+ return ManifestIconItem({ label: key, key: domKey, value });
+ case MANIFEST_MEMBER_VALUE_TYPES.URL:
+ return ManifestUrlItem({ label: key, key: domKey, value });
+ case MANIFEST_MEMBER_VALUE_TYPES.STRING:
+ default:
+ return ManifestItem({ label: key, key: domKey }, value);
+ }
+ }
+
+ renderItemSections() {
+ const { identity, icons, presentation } = this.props;
+
+ const manifestMembers = [
+ { localizationId: "manifest-item-identity", items: identity },
+ { localizationId: "manifest-item-presentation", items: presentation },
+ { localizationId: "manifest-item-icons", items: icons },
+ ];
+
+ return manifestMembers.map(({ localizationId, items }, index) => {
+ return ManifestSection(
+ {
+ key: `manifest-section-${index + 1}`,
+ title: l10n.getString(localizationId),
+ },
+ // NOTE: this table should probably be its own component, to keep
+ // the same level of abstraction as with the validation issues
+ // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1577138
+ table({}, tbody({}, items.map(this.renderMember)))
+ );
+ });
+ }
+
+ render() {
+ const { url } = this.props;
+
+ return article(
+ { className: "js-manifest" },
+ Localized(
+ {
+ id: "manifest-view-header",
+ },
+ h1({ className: "app-page__title" })
+ ),
+ ManifestJsonLink({ url }),
+ this.renderIssueSection(),
+ this.renderItemSections()
+ );
+ }
+}
+
+// Exports
+module.exports = Manifest;
diff --git a/devtools/client/application/src/components/manifest/ManifestColorItem.css b/devtools/client/application/src/components/manifest/ManifestColorItem.css
new file mode 100644
index 0000000000..92dadec271
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestColorItem.css
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-item__color {
+ /* NOTE: platform converts any color format that is in the manifest to
+ hexadecimal, so we can uppercase */
+ text-transform: uppercase;
+ direction: ltr; /* force LTR so the # stays at the beginning of the hex number */
+ display: inline-block;
+}
+
+.manifest-item__color::before {
+ display: inline-block;
+ content: '';
+ background-color: #fff;
+ background-image: linear-gradient(var(--color-value), var(--color-value)), /* injected via React */
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+ background-size: 6px 6px;
+ background-position: 0 0, 3px 3px;
+ border-radius: 50%;
+ width: calc(var(--base-unit) * 3);
+ height: calc(var(--base-unit) * 3);
+ margin-block-start: calc(var(--base-unit) * -0.5);
+ margin-inline-end: var(--base-unit);
+ box-shadow: 0 0 0 1px var(--theme-splitter-color);
+ vertical-align: middle;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestColorItem.js b/devtools/client/application/src/components/manifest/ManifestColorItem.js
new file mode 100644
index 0000000000..ac4b54e82f
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestColorItem.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+
+/**
+ * This component displays a Manifest member which holds a color value
+ */
+class ManifestColorItem extends PureComponent {
+ static get propTypes() {
+ return {
+ ...Types.manifestItemColor, // { label, value }
+ };
+ }
+
+ renderColor() {
+ let { value } = this.props;
+ if (!value) {
+ return null;
+ }
+
+ // Truncate colors in #rrggbbaa format to #rrggbb
+ if (value.length === 9 && value.toLowerCase().endsWith("ff")) {
+ value = value.slice(0, 7);
+ }
+
+ /* div instead of span because CSS `direction` works with block elements */
+ return div(
+ {
+ className: "manifest-item__color",
+ style: { "--color-value": value },
+ },
+ value
+ );
+ }
+
+ render() {
+ const { label } = this.props;
+ return ManifestItem({ label }, this.renderColor());
+ }
+}
+
+module.exports = ManifestColorItem;
diff --git a/devtools/client/application/src/components/manifest/ManifestEmpty.js b/devtools/client/application/src/components/manifest/ManifestEmpty.js
new file mode 100644
index 0000000000..3e0eb2de48
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestEmpty.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ a,
+ article,
+ aside,
+ div,
+ h1,
+ img,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const DOC_URL =
+ "https://developer.mozilla.org/en-US/docs/Web/Manifest" +
+ "?utm_source=devtools&utm_medium=sw-panel-blank";
+
+/**
+ * This component displays help information when no manifest is found for the
+ * current target.
+ */
+class ManifestEmpty extends PureComponent {
+ openDocumentation() {
+ openDocLink(DOC_URL);
+ }
+
+ render() {
+ return article(
+ { className: "app-page__icon-container js-manifest-empty" },
+ aside(
+ {},
+ Localized(
+ {
+ id: "sidebar-item-manifest",
+ attrs: {
+ alt: true,
+ },
+ },
+ img({
+ className: "app-page__icon",
+ src: "chrome://devtools/skin/images/application-manifest.svg",
+ })
+ )
+ ),
+ div(
+ {},
+ Localized(
+ {
+ id: "manifest-empty-intro2",
+ },
+ h1({ className: "app-page__title" })
+ ),
+ p(
+ {},
+ Localized(
+ { id: "manifest-empty-intro-link" },
+ a({
+ onClick: () => this.openDocumentation(),
+ })
+ )
+ ),
+ Localized({ id: "manifest-non-existing" }, p({}))
+ )
+ );
+ }
+}
+
+// Exports
+module.exports = ManifestEmpty;
diff --git a/devtools/client/application/src/components/manifest/ManifestIconItem.css b/devtools/client/application/src/components/manifest/ManifestIconItem.css
new file mode 100644
index 0000000000..a2bbfd9d34
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIconItem.css
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-item__icon {
+ max-width: 100%;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestIconItem.js b/devtools/client/application/src/components/manifest/ManifestIconItem.js
new file mode 100644
index 0000000000..a525fbcdb9
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIconItem.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ br,
+ code,
+ img,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+const {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+
+/**
+ * This component displays a Manifest member which holds a color value
+ */
+class ManifestIconItem extends PureComponent {
+ static get propTypes() {
+ return {
+ // {
+ // label: { contentType, sizes },
+ // value: { src, purpose }
+ // }
+ ...Types.manifestItemIcon,
+ };
+ }
+
+ getLocalizedImgTitle() {
+ const { sizes } = this.props.label;
+
+ return sizes && sizes.length
+ ? l10n.getString("manifest-icon-img-title", { sizes })
+ : l10n.getString("manifest-icon-img-title-no-sizes");
+ }
+
+ renderLabel() {
+ const { contentType, sizes } = this.props.label;
+
+ // sinze both `contentType` and `sizes` may be undefined, we don't need to
+ // render the <br> if one –or both– are not to be displayed
+ const shallRenderBr = contentType && sizes;
+
+ return [
+ sizes ? sizes : null,
+ shallRenderBr ? br({ key: "label-br" }) : null,
+ contentType ? contentType : null,
+ ];
+ }
+
+ render() {
+ const { src, purpose } = this.props.value;
+
+ return ManifestItem(
+ {
+ label: this.renderLabel(),
+ },
+ Localized(
+ {
+ id: "manifest-icon-img",
+ attrs: {
+ alt: true,
+ },
+ },
+ img({
+ className: "manifest-item__icon",
+ src,
+ title: this.getLocalizedImgTitle(),
+ })
+ ),
+ br({}),
+ Localized(
+ {
+ id: "manifest-icon-purpose",
+ code: code({}),
+ $purpose: purpose,
+ },
+ span({})
+ )
+ );
+ }
+}
+
+module.exports = ManifestIconItem;
diff --git a/devtools/client/application/src/components/manifest/ManifestIssue.css b/devtools/client/application/src/components/manifest/ManifestIssue.css
new file mode 100644
index 0000000000..96bcdae5dd
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIssue.css
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-issue__icon {
+ -moz-context-properties: fill;
+ width: calc(var(--base-unit) * 3);
+ height: calc(var(--base-unit) * 3);
+}
+
+.manifest-issue__icon--warning {
+ fill: var(--theme-icon-warning-color);
+}
+
+.manifest-issue__icon--error {
+ fill: var(--theme-icon-error-color);
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestIssue.js b/devtools/client/application/src/components/manifest/ManifestIssue.js
new file mode 100644
index 0000000000..6a9680d604
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIssue.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ img,
+ li,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ MANIFEST_ISSUE_LEVELS,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+/**
+ * A Manifest validation issue (warning, error)
+ */
+class ManifestIssue extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ ...Types.manifestIssue, // { level, message }
+ };
+ }
+
+ getIconData(level) {
+ switch (level) {
+ case MANIFEST_ISSUE_LEVELS.WARNING:
+ return {
+ src: "chrome://devtools/skin/images/alert-small.svg",
+ localizationId: "icon-warning",
+ };
+ case MANIFEST_ISSUE_LEVELS.ERROR:
+ default:
+ return {
+ src: "resource://devtools-shared-images/error-small.svg",
+ localizationId: "icon-error",
+ };
+ }
+ }
+
+ render() {
+ const { level, message, className } = this.props;
+ const icon = this.getIconData(level);
+
+ return li(
+ { className: `js-manifest-issue ${className ? className : ""}` },
+ Localized(
+ { id: icon.localizationId, attrs: { alt: true, title: true } },
+ img({
+ className: `manifest-issue__icon manifest-issue__icon--${level}`,
+ src: icon.src,
+ })
+ ),
+ span({}, message)
+ );
+ }
+}
+
+// Exports
+module.exports = ManifestIssue;
diff --git a/devtools/client/application/src/components/manifest/ManifestIssueList.css b/devtools/client/application/src/components/manifest/ManifestIssueList.css
new file mode 100644
index 0000000000..ccb3f08df5
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIssueList.css
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-issues {
+ list-style-type: none;
+ padding-inline-start: 0;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-gap: var(--base-unit);
+}
+
+.manifest-issues__item {
+ display: contents;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestIssueList.js b/devtools/client/application/src/components/manifest/ManifestIssueList.js
new file mode 100644
index 0000000000..4b3d1f46c8
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIssueList.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ ul,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ MANIFEST_ISSUE_LEVELS,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const ManifestIssue = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIssue.js")
+);
+
+/**
+ * A collection of manifest issues (errors, warnings)
+ */
+class ManifestIssueList extends PureComponent {
+ static get propTypes() {
+ return {
+ issues: Types.manifestIssueArray.isRequired,
+ };
+ }
+
+ // group the errors by level, and order by severity
+ groupIssuesByLevel() {
+ const { issues } = this.props;
+
+ const errors = issues.filter(x => x.level === MANIFEST_ISSUE_LEVELS.ERROR);
+ const warnings = issues.filter(
+ x => x.level === MANIFEST_ISSUE_LEVELS.WARNING
+ );
+
+ return [errors, warnings];
+ }
+
+ render() {
+ const groups = this.groupIssuesByLevel().filter(list => !!list.length);
+
+ return groups.map((list, listIndex) => {
+ return ul(
+ {
+ className: "manifest-issues js-manifest-issues",
+ key: `issuelist-${listIndex}`,
+ },
+ list.map((issue, issueIndex) =>
+ ManifestIssue({
+ className: "manifest-issues__item",
+ key: `issue-${issueIndex}`,
+ ...issue,
+ })
+ )
+ );
+ });
+ }
+}
+
+// Exports
+module.exports = ManifestIssueList;
diff --git a/devtools/client/application/src/components/manifest/ManifestItem.css b/devtools/client/application/src/components/manifest/ManifestItem.css
new file mode 100644
index 0000000000..94da03e9b9
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestItem.css
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-item {
+ vertical-align: baseline;
+}
+
+.manifest-item__label {
+ box-sizing: border-box;
+ min-width: calc(var(--base-unit) * 32);
+ padding-inline-end: calc(var(--base-unit) * 4);
+ padding-inline-start: 0;
+ vertical-align: top;
+ color: var(--theme-text-color-alt);
+ font-weight: inherit;
+ text-align: start;
+}
+
+.manifest-item__value {
+ word-break: break-all;
+ vertical-align: top;
+}
+
+.manifest-item__label,
+.manifest-item__value {
+ padding-block: calc(var(--base-unit) * 1);
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestItem.js b/devtools/client/application/src/components/manifest/ManifestItem.js
new file mode 100644
index 0000000000..96c47a1a7f
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestItem.js
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ tr,
+ td,
+ th,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+/**
+ * This component displays a key-value data pair from a manifest
+ */
+class ManifestItem extends PureComponent {
+ static get propTypes() {
+ return {
+ label: PropTypes.node.isRequired,
+ children: PropTypes.node,
+ };
+ }
+
+ render() {
+ const { children, label } = this.props;
+ return tr(
+ {
+ className: "manifest-item js-manifest-item",
+ },
+ th(
+ {
+ className: "manifest-item__label js-manifest-item-label",
+ scope: "row",
+ },
+ label
+ ),
+ td(
+ { className: "manifest-item__value js-manifest-item-content" },
+ children
+ )
+ );
+ }
+}
+
+// Exports
+module.exports = ManifestItem;
diff --git a/devtools/client/application/src/components/manifest/ManifestJsonLink.css b/devtools/client/application/src/components/manifest/ManifestJsonLink.css
new file mode 100644
index 0000000000..52343226f9
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestJsonLink.css
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-json-link {
+ /* this is so it has an implicit width and the link inside gets to truncate
+ with an ellipsis */
+ display: grid;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestJsonLink.js b/devtools/client/application/src/components/manifest/ManifestJsonLink.js
new file mode 100644
index 0000000000..d35924b5c3
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestJsonLink.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const {
+ a,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+/**
+ * This component displays an "Open JSON" link for the Manifest
+ */
+class ManifestJsonLink extends PureComponent {
+ static get propTypes() {
+ return {
+ url: PropTypes.string.isRequired,
+ };
+ }
+
+ get shouldRenderLink() {
+ const { url } = this.props;
+ // Firefox blocks the loading of Data URLs with mimetypes manifest+json unless
+ // explicitly typed by the user in the address bar.
+ // So we are not showing the link in this case.
+ // See more details in this post:
+ // https://blog.mozilla.org/security/2017/11/27/blocking-top-level-navigations-data-urls-firefox-59/
+ return !url.startsWith("data:");
+ }
+
+ renderLink() {
+ const { url } = this.props;
+
+ return a(
+ {
+ className: "js-manifest-json-link devtools-ellipsis-text",
+ href: "#",
+ title: url,
+ onClick: () => openDocLink(url),
+ },
+ url
+ );
+ }
+
+ render() {
+ return p(
+ { className: "manifest-json-link" },
+ this.shouldRenderLink
+ ? this.renderLink()
+ : Localized({ id: "manifest-json-link-data-url" })
+ );
+ }
+}
+
+module.exports = ManifestJsonLink;
diff --git a/devtools/client/application/src/components/manifest/ManifestLoader.css b/devtools/client/application/src/components/manifest/ManifestLoader.css
new file mode 100644
index 0000000000..4f728ff6e3
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestLoader.css
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-loader {
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight);
+}
+
+.manifest-loader__load {
+ /* TODO: implement a spinner when tackling the UX review bug
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1566023 */
+ text-align: center;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestLoader.js b/devtools/client/application/src/components/manifest/ManifestLoader.js
new file mode 100644
index 0000000000..9183b50ff3
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestLoader.js
@@ -0,0 +1,108 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ aside,
+ h1,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ fetchManifest,
+} = require("resource://devtools/client/application/src/actions/manifest.js");
+
+class ManifestLoader extends PureComponent {
+ static get propTypes() {
+ return {
+ // these props get automatically injected via `connect`
+ dispatch: PropTypes.func.isRequired,
+ error: PropTypes.string,
+ hasFetchedManifest: PropTypes.bool.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ this.loadManifestIfNeeded();
+ }
+
+ componentDidUpdate() {
+ this.loadManifestIfNeeded();
+ }
+
+ loadManifestIfNeeded() {
+ const { isLoading, hasFetchedManifest } = this.props;
+ const shallLoad = !isLoading && !hasFetchedManifest;
+ if (shallLoad) {
+ this.props.dispatch(fetchManifest());
+ }
+ }
+
+ renderResult() {
+ return Localized(
+ { id: "manifest-loaded-ok" },
+ p({ className: "js-manifest-loaded-ok" })
+ );
+ }
+
+ renderError() {
+ const { error } = this.props;
+
+ return [
+ Localized(
+ {
+ id: "manifest-loaded-error",
+ key: "manifest-error-label",
+ },
+ h1({ className: "js-manifest-loaded-error app-page__title" })
+ ),
+ p({ className: "technical-text", key: "manifest-error-message" }, error),
+ ];
+ }
+
+ render() {
+ const { error, isLoading } = this.props;
+
+ const loadingDOM = isLoading
+ ? Localized(
+ { id: "manifest-loading" },
+ p({ className: "manifest-loader__load js-manifest-loading" })
+ )
+ : null;
+
+ const errorDOM = error ? this.renderError() : null;
+ const resultDOM = !isLoading && !error ? this.renderResult() : null;
+
+ return aside(
+ { className: "manifest-loader" },
+ loadingDOM,
+ errorDOM,
+ resultDOM
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+const mapStateToProps = state => ({
+ error: state.manifest.errorMessage,
+ hasFetchedManifest: typeof state.manifest.manifest !== "undefined",
+ isLoading: state.manifest.isLoading,
+});
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ManifestLoader);
diff --git a/devtools/client/application/src/components/manifest/ManifestPage.js b/devtools/client/application/src/components/manifest/ManifestPage.js
new file mode 100644
index 0000000000..60caea591e
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestPage.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ section,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const ManifestLoader = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestLoader.js")
+);
+const Manifest = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/Manifest.js")
+);
+const ManifestEmpty = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestEmpty.js")
+);
+
+class ManifestPage extends PureComponent {
+ static get propTypes() {
+ return {
+ // these props are automatically injected via connect
+ hasLoadingFailed: PropTypes.bool.isRequired,
+ isManifestLoading: PropTypes.bool.isRequired,
+ manifest: PropTypes.shape(Types.manifest),
+ };
+ }
+
+ get shouldShowLoader() {
+ const { isManifestLoading, hasLoadingFailed } = this.props;
+ const mustLoadManifest = typeof this.props.manifest === "undefined";
+ return isManifestLoading || mustLoadManifest || hasLoadingFailed;
+ }
+
+ renderManifest() {
+ const { manifest } = this.props;
+ return manifest ? Manifest({ ...manifest }) : ManifestEmpty({});
+ }
+
+ render() {
+ const { manifest } = this.props;
+
+ return section(
+ {
+ className: `app-page js-manifest-page ${
+ !manifest ? "app-page--empty" : ""
+ }`,
+ },
+ this.shouldShowLoader ? ManifestLoader({}) : this.renderManifest()
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ hasLoadingFailed: !!state.manifest.errorMessage,
+ isManifestLoading: state.manifest.isLoading,
+ manifest: state.manifest.manifest,
+ };
+}
+
+// Exports
+module.exports = connect(mapStateToProps)(ManifestPage);
diff --git a/devtools/client/application/src/components/manifest/ManifestSection.css b/devtools/client/application/src/components/manifest/ManifestSection.css
new file mode 100644
index 0000000000..479a6d1f79
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestSection.css
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-section {
+ padding-block: calc(var(--base-unit) * 2);
+ width: 100%;
+ border-spacing: calc(var(--base-unit) * 2) 0;
+ font-size: var(--body-10-font-size);
+ font-weight: var(--body-10-font-weight);
+}
+
+.manifest-section--empty {
+ padding-block-end: 0;
+}
+
+.manifest-section:not(:last-child) {
+ border-bottom: 1px solid var(--separator-color);
+}
+
+.manifest-section__title {
+ font-size: var(--title-10-font-size);
+ font-weight: var(--title-10-font-weight);
+ margin: 0;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestSection.js b/devtools/client/application/src/components/manifest/ManifestSection.js
new file mode 100644
index 0000000000..4aa92c6a15
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestSection.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ h2,
+ section,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+/**
+ * A section of a manifest in the form of a captioned table.
+ */
+class ManifestSection extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ title: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const { children, title } = this.props;
+ const isEmpty = !children || children.length === 0;
+
+ return section(
+ {
+ className: `manifest-section ${
+ isEmpty ? "manifest-section--empty" : ""
+ }`,
+ },
+ h2({ className: "manifest-section__title" }, title),
+ children
+ );
+ }
+}
+
+// Exports
+module.exports = ManifestSection;
diff --git a/devtools/client/application/src/components/manifest/ManifestUrlItem.css b/devtools/client/application/src/components/manifest/ManifestUrlItem.css
new file mode 100644
index 0000000000..9702e7e261
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestUrlItem.css
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.manifest-item__url {
+ direction: ltr; /* force LTR so the # stays at the beginning of the hex number */
+ display: inline-block;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestUrlItem.js b/devtools/client/application/src/components/manifest/ManifestUrlItem.js
new file mode 100644
index 0000000000..03705d441a
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestUrlItem.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+
+/**
+ * This component displays a Manifest member which holds a URL
+ */
+class ManifestUrlItem extends PureComponent {
+ static get propTypes() {
+ return {
+ ...Types.manifestItemUrl, // { label, value }
+ };
+ }
+
+ render() {
+ const { label, value } = this.props;
+ return ManifestItem(
+ { label },
+ div({ className: "manifest-item__url" }, value)
+ );
+ }
+}
+
+module.exports = ManifestUrlItem;
diff --git a/devtools/client/application/src/components/manifest/moz.build b/devtools/client/application/src/components/manifest/moz.build
new file mode 100644
index 0000000000..bb799cbfc4
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/moz.build
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "Manifest.js",
+ "ManifestColorItem.js",
+ "ManifestEmpty.js",
+ "ManifestIconItem.js",
+ "ManifestIssue.js",
+ "ManifestIssueList.js",
+ "ManifestItem.js",
+ "ManifestJsonLink.js",
+ "ManifestLoader.js",
+ "ManifestPage.js",
+ "ManifestSection.js",
+ "ManifestUrlItem.js",
+)
diff --git a/devtools/client/application/src/components/moz.build b/devtools/client/application/src/components/moz.build
new file mode 100644
index 0000000000..361ec01204
--- /dev/null
+++ b/devtools/client/application/src/components/moz.build
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "routing",
+ "manifest",
+ "service-workers",
+ "ui",
+]
+
+DevToolsModules(
+ "App.js",
+)
diff --git a/devtools/client/application/src/components/routing/PageSwitcher.css b/devtools/client/application/src/components/routing/PageSwitcher.css
new file mode 100644
index 0000000000..e713adb1bf
--- /dev/null
+++ b/devtools/client/application/src/components/routing/PageSwitcher.css
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+/*
+ * Page container for worker + manifest views
+ */
+
+.app-page {
+ padding: calc(var(--base-unit) * 3) calc(var(--base-unit) * 8);
+ user-select: none;
+ overflow-y: auto;
+}
+
+.app-page--empty {
+ display: grid;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--body-10-font-size);
+ color: var(--theme-toolbar-color);
+}
+
+.app-page__title {
+ font-size: var(--title-20-font-size);
+ font-weight: var(--title-20-font-weight);
+ margin: 0;
+}
+
+.app-page__icon-container {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-column-gap: calc(var(--base-unit) * 4);
+}
+
+.app-page__icon {
+ width: calc(var(--base-unit) * 10);
+ height: calc(var(--base-unit) * 10);
+
+ fill: var(--dimmed-icon-color);
+ -moz-context-properties: fill;
+
+ /* alignment fix for text to compensate for low baseline */
+ margin-block-start: var(--base-unit);
+}
diff --git a/devtools/client/application/src/components/routing/PageSwitcher.js b/devtools/client/application/src/components/routing/PageSwitcher.js
new file mode 100644
index 0000000000..9305744da9
--- /dev/null
+++ b/devtools/client/application/src/components/routing/PageSwitcher.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const ManifestPage = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestPage.js")
+);
+const WorkersPage = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/WorkersPage.js")
+);
+
+class PageSwitcher extends PureComponent {
+ static get propTypes() {
+ return {
+ page: Types.page.isRequired,
+ };
+ }
+
+ render() {
+ let component = null;
+
+ switch (this.props.page) {
+ case PAGE_TYPES.MANIFEST:
+ component = ManifestPage({});
+ break;
+ case PAGE_TYPES.SERVICE_WORKERS:
+ component = WorkersPage({});
+ break;
+ default:
+ console.error("Unknown path. Can not direct to a page.");
+ return null;
+ }
+
+ return component;
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ page: state.ui.selectedPage,
+ };
+}
+
+module.exports = connect(mapStateToProps)(PageSwitcher);
diff --git a/devtools/client/application/src/components/routing/Sidebar.css b/devtools/client/application/src/components/routing/Sidebar.css
new file mode 100644
index 0000000000..872f5cca86
--- /dev/null
+++ b/devtools/client/application/src/components/routing/Sidebar.css
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+/*
+ * Sidebar list container
+ */
+.sidebar {
+ background-color: var(--bg-color);
+}
+
+/* vertical layout -> the sidebar is the first row */
+@media(max-width: 700px) {
+ .sidebar {
+ border-block-end: 1px solid var(--separator-color);
+ }
+}
+
+/* wide layout -> the sidebar occupies a whole column on the side */
+@media(min-width: 701px) {
+ .sidebar {
+ min-height: 100vh;
+ border-inline-end: 1px solid var(--separator-color);
+ }
+}
+
+.sidebar__list {
+ list-style: none;
+ padding: 0;
+ font-size: var(--body-10-font-size);
+ font-weight: var(--body-10-font-weight);
+}
diff --git a/devtools/client/application/src/components/routing/Sidebar.js b/devtools/client/application/src/components/routing/Sidebar.js
new file mode 100644
index 0000000000..99d260571e
--- /dev/null
+++ b/devtools/client/application/src/components/routing/Sidebar.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ aside,
+ ul,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const SidebarItem = createFactory(
+ require("resource://devtools/client/application/src/components/routing/SidebarItem.js")
+);
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+class Sidebar extends PureComponent {
+ static get propTypes() {
+ return {
+ // this prop is automatically injected via connect
+ selectedPage: Types.page.isRequired,
+ };
+ }
+
+ render() {
+ const navItems = [PAGE_TYPES.SERVICE_WORKERS, PAGE_TYPES.MANIFEST];
+
+ const isSelected = page => {
+ return page === this.props.selectedPage;
+ };
+
+ return aside(
+ {
+ className: "sidebar js-sidebar",
+ },
+ ul(
+ {
+ className: "sidebar__list",
+ },
+ navItems.map(page => {
+ return SidebarItem({
+ page,
+ key: `sidebar-item-${page}`,
+ isSelected: isSelected(page),
+ });
+ })
+ )
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ selectedPage: state.ui.selectedPage,
+ };
+}
+
+module.exports = connect(mapStateToProps)(Sidebar);
diff --git a/devtools/client/application/src/components/routing/SidebarItem.css b/devtools/client/application/src/components/routing/SidebarItem.css
new file mode 100644
index 0000000000..f1852748ab
--- /dev/null
+++ b/devtools/client/application/src/components/routing/SidebarItem.css
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+/*
+ * Sidebar list items
+ */
+
+.sidebar-item {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-gap: var(--base-unit);
+ padding: calc(var(--base-unit)) calc(var(--base-unit) * 6);
+ user-select: none;
+ cursor: pointer;
+}
+
+.sidebar-item--selected {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.sidebar-item:not(.sidebar-item--selected):hover {
+ background-color: var(--highlight-color);
+}
+
+.sidebar-item__icon {
+ height: calc(var(--base-unit) * 4);
+ width: calc(var(--base-unit) * 4);
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
diff --git a/devtools/client/application/src/components/routing/SidebarItem.js b/devtools/client/application/src/components/routing/SidebarItem.js
new file mode 100644
index 0000000000..5683856aaf
--- /dev/null
+++ b/devtools/client/application/src/components/routing/SidebarItem.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ img,
+ li,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Actions = require("resource://devtools/client/application/src/actions/index.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const ICONS = {
+ [PAGE_TYPES.MANIFEST]:
+ "chrome://devtools/skin/images/application-manifest.svg",
+ [PAGE_TYPES.SERVICE_WORKERS]:
+ "chrome://devtools/skin/images/debugging-workers.svg",
+};
+
+const LOCALIZATION_IDS = {
+ [PAGE_TYPES.MANIFEST]: "sidebar-item-manifest",
+ [PAGE_TYPES.SERVICE_WORKERS]: "sidebar-item-service-workers",
+};
+
+class SidebarItem extends PureComponent {
+ static get propTypes() {
+ return {
+ page: Types.page.isRequired,
+ isSelected: PropTypes.bool.isRequired,
+ // this prop is automatically injected via connect
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { isSelected, page } = this.props;
+
+ return li(
+ {
+ className: `sidebar-item js-sidebar-${page} ${
+ isSelected ? "sidebar-item--selected" : ""
+ }`,
+ onClick: () => {
+ const { dispatch } = this.props;
+ dispatch(Actions.updateSelectedPage(page));
+ },
+ role: "link",
+ },
+ Localized(
+ {
+ id: LOCALIZATION_IDS[page],
+ attrs: {
+ alt: true,
+ title: true,
+ },
+ },
+ img({
+ src: ICONS[page],
+ className: "sidebar-item__icon",
+ })
+ ),
+ Localized(
+ {
+ id: LOCALIZATION_IDS[page],
+ attrs: {
+ title: true,
+ },
+ },
+ span({ className: "devtools-ellipsis-text" })
+ )
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+module.exports = connect(mapDispatchToProps)(SidebarItem);
diff --git a/devtools/client/application/src/components/routing/moz.build b/devtools/client/application/src/components/routing/moz.build
new file mode 100644
index 0000000000..7e22985614
--- /dev/null
+++ b/devtools/client/application/src/components/routing/moz.build
@@ -0,0 +1,5 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules("PageSwitcher.js", "Sidebar.js", "SidebarItem.js")
diff --git a/devtools/client/application/src/components/service-workers/Registration.css b/devtools/client/application/src/components/service-workers/Registration.css
new file mode 100644
index 0000000000..84b6de58e1
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/Registration.css
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * The current layout of a registration is
+ *
+ * +------+----------------------+----------------+
+ * | Header - scope + timestamp | Unregister_btn |
+ * +------+----------------------+----------------|
+ * | worker 1 |
+ | worker 2 |
+ | ... |
+ +----------------------------------------------+
+ | Unregister btn |
+ +----------------------------------------------+
+ */
+
+.registration {
+ line-height: 1.5;
+ font-size: var(--body-10-font-size);
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ grid-template-rows: minmax(calc(var(--base-unit) * 6), auto) 1fr auto;
+ grid-column-gap: calc(4 * var(--base-unit));
+ grid-row-gap: calc(2 * var(--base-unit));
+ grid-template-areas: "header header-controls"
+ "workers workers"
+ "footer-controls footer-controls";
+}
+
+/* vertical layout */
+@media(max-width: 700px) {
+ .registration__controls {
+ grid-area: footer-controls;
+ justify-self: end;
+ }
+}
+
+/* wide layout */
+@media(min-width: 701px) {
+ .registration__controls {
+ grid-area: header-controls;
+ }
+}
+
+.registration__header {
+ grid-area: header;
+}
+
+.registration__scope {
+ font-size: var(--title-10-font-size);
+ font-weight: var(--title-10-font-weight);
+ user-select: text;
+ margin: 0;
+
+ grid-area: scope;
+}
+
+.registration__updated-time {
+ color: var(--theme-text-color-alt);
+ grid-area: timestamp;
+}
+
+.registration__workers {
+ grid-area: workers;
+ list-style-type: none;
+ padding: 0;
+}
+
+.registration__workers-item:not(:first-child) {
+ margin-block-start: calc(var(--base-unit) * 2);
+}
diff --git a/devtools/client/application/src/components/service-workers/Registration.js b/devtools/client/application/src/components/service-workers/Registration.js
new file mode 100644
index 0000000000..97569f57e2
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/Registration.js
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ article,
+ aside,
+ h2,
+ header,
+ li,
+ p,
+ time,
+ ul,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ getUnicodeUrl,
+} = require("resource://devtools/client/shared/unicode-url.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const {
+ unregisterWorker,
+} = require("resource://devtools/client/application/src/actions/workers.js");
+
+const UIButton = createFactory(
+ require("resource://devtools/client/application/src/components/ui/UIButton.js")
+);
+
+const Worker = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/Worker.js")
+);
+
+/**
+ * This component is dedicated to display a service worker registration, along
+ * the list of attached workers to it.
+ * It displays information about the registration as well as an Unregister
+ * button.
+ */
+class Registration extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ isDebugEnabled: PropTypes.bool.isRequired,
+ registration: PropTypes.shape(Types.registration).isRequired,
+ // this prop get automatically injected via `connect`
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.unregister = this.unregister.bind(this);
+ }
+
+ unregister() {
+ this.props.dispatch(unregisterWorker(this.props.registration));
+ }
+
+ isActive() {
+ const { workers } = this.props.registration;
+ return workers.some(
+ x => x.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED
+ );
+ }
+
+ formatScope(scope) {
+ const [, remainder] = getUnicodeUrl(scope).split("://");
+ // remove the last slash from the url, if present
+ // or return the full scope if there's no remainder
+ return remainder ? remainder.replace(/\/$/, "") : scope;
+ }
+
+ render() {
+ const { registration, isDebugEnabled, className } = this.props;
+
+ const unregisterButton = this.isActive()
+ ? Localized(
+ { id: "serviceworker-worker-unregister" },
+ UIButton({
+ onClick: this.unregister,
+ className: "js-unregister-button",
+ })
+ )
+ : null;
+
+ const lastUpdated = registration.lastUpdateTime
+ ? Localized(
+ {
+ id: "serviceworker-worker-updated",
+ // XXX: $date should normally be a Date object, but we pass the timestamp as a
+ // workaround. See Bug 1465718. registration.lastUpdateTime is in microseconds,
+ // convert to a valid timestamp in milliseconds by dividing by 1000.
+ $date: registration.lastUpdateTime / 1000,
+ time: time({ className: "js-sw-updated" }),
+ },
+ p({ className: "registration__updated-time" })
+ )
+ : null;
+
+ const scope = h2(
+ {
+ title: registration.scope,
+ className: "registration__scope js-sw-scope devtools-ellipsis-text",
+ },
+ this.formatScope(registration.scope)
+ );
+
+ return li(
+ { className: className ? className : "" },
+ article(
+ { className: "registration js-sw-container" },
+ header({ className: "registration__header" }, scope, lastUpdated),
+ aside({ className: "registration__controls" }, unregisterButton),
+ // render list of workers
+ ul(
+ { className: "registration__workers" },
+ registration.workers.map(worker => {
+ return li(
+ {
+ key: worker.id,
+ className: "registration__workers-item",
+ },
+ Worker({
+ worker,
+ isDebugEnabled,
+ })
+ );
+ })
+ )
+ )
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+module.exports = connect(mapDispatchToProps)(Registration);
diff --git a/devtools/client/application/src/components/service-workers/RegistrationList.css b/devtools/client/application/src/components/service-workers/RegistrationList.css
new file mode 100644
index 0000000000..021ed7e351
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/RegistrationList.css
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.aboutdebugging-plug {
+ padding-block: calc(var(--base-unit) * 3);
+ border-block-start: 1px solid var(--separator-color);
+
+ /* display flex to handle showing the icon with ::before */
+ display: flex;
+ flex-direction: row;
+ column-gap: calc(var(--base-unit) * 2);
+ align-items: baseline;
+ font-size: var(--body-10-font-size);
+ font-weight: var(--body-10-font-weight);
+}
+
+.aboutdebugging-plug::before {
+ flex: 0 0 auto;
+ width: calc(var(--base-unit) * 4);
+ height: calc(var(--base-unit) * 4);
+ content: "";
+ -moz-context-properties: fill;
+ fill: currentColor;
+ background-image: url(chrome://global/skin/icons/developer.svg);
+ /* the icon size is taller than the line-height of the text. Since the
+ text can occupy multiple lines, and we want to keep the icon aligned
+ with respect to the first line, instead of align-items: center in
+ .aboutdebugging-plug, we use baseline, and fine tune the position here. */
+ position: relative;
+ top: 3px;
+}
+
+.registrations-container {
+ flex-grow: 1;
+}
+
+.registrations-container__list {
+ padding-inline-start: 0;
+}
+
+.registrations-container__item {
+ list-style-type: none;
+ margin: 0;
+ padding: calc(var(--base-unit) * 5) 0;
+}
+
+.registrations-container__item:first-child {
+ padding-top: 0;
+}
+
+.registrations-container__item:not(:last-child) {
+ border-bottom: 1px solid var(--separator-color);
+}
diff --git a/devtools/client/application/src/components/service-workers/RegistrationList.js b/devtools/client/application/src/components/service-workers/RegistrationList.js
new file mode 100644
index 0000000000..ed89c9cff3
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/RegistrationList.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ openTrustedLink,
+} = require("resource://devtools/client/shared/link.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ a,
+ article,
+ footer,
+ h1,
+ p,
+ ul,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const Registration = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/Registration.js")
+);
+
+/**
+ * This component handles the list of service workers displayed in the application panel
+ * and also displays a suggestion to use about debugging for debugging other service
+ * workers.
+ */
+class RegistrationList extends PureComponent {
+ static get propTypes() {
+ return {
+ canDebugWorkers: PropTypes.bool.isRequired,
+ registrations: Types.registrationArray.isRequired,
+ };
+ }
+
+ render() {
+ const { canDebugWorkers, registrations } = this.props;
+
+ return [
+ article(
+ {
+ className: "registrations-container",
+ key: "registrations-container",
+ },
+ Localized(
+ { id: "serviceworker-list-header" },
+ h1({
+ className: "app-page__title",
+ })
+ ),
+ ul(
+ { className: "registrations-container__list" },
+ registrations.map(registration =>
+ Registration({
+ key: registration.id,
+ isDebugEnabled: canDebugWorkers,
+ registration,
+ className: "registrations-container__item",
+ })
+ )
+ )
+ ),
+
+ footer(
+ { className: "aboutdebugging-plug" },
+ Localized(
+ {
+ id: "serviceworker-list-aboutdebugging",
+ key: "serviceworkerlist-footer",
+ a: a({
+ className: "aboutdebugging-plug__link",
+ onClick: () => openTrustedLink("about:debugging#workers"),
+ }),
+ },
+ p({})
+ )
+ ),
+ ];
+ }
+}
+
+// Exports
+module.exports = RegistrationList;
diff --git a/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js b/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js
new file mode 100644
index 0000000000..2454f121e8
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ openDocLink,
+ openTrustedLink,
+} = require("resource://devtools/client/shared/link.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ a,
+ article,
+ aside,
+ div,
+ h1,
+ img,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ services,
+} = require("resource://devtools/client/application/src/modules/application-services.js");
+
+const DOC_URL =
+ "https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers" +
+ "?utm_source=devtools&utm_medium=sw-panel-blank";
+
+/**
+ * This component displays help information when no service workers are found for the
+ * current target.
+ */
+class RegistrationListEmpty extends PureComponent {
+ switchToConsole() {
+ services.selectTool("webconsole");
+ }
+
+ switchToDebugger() {
+ services.selectTool("jsdebugger");
+ }
+
+ openAboutDebugging() {
+ openTrustedLink("about:debugging#workers");
+ }
+
+ openDocumentation() {
+ openDocLink(DOC_URL);
+ }
+
+ render() {
+ return article(
+ { className: "app-page__icon-container js-registration-list-empty" },
+ aside(
+ {},
+ Localized(
+ {
+ id: "sidebar-item-service-workers",
+ attrs: {
+ alt: true,
+ },
+ },
+ img({
+ className: "app-page__icon",
+ src: "chrome://devtools/skin/images/debugging-workers.svg",
+ })
+ )
+ ),
+ div(
+ {},
+ Localized(
+ {
+ id: "serviceworker-empty-intro2",
+ },
+ h1({ className: "app-page__title" })
+ ),
+ Localized(
+ {
+ id: "serviceworker-empty-suggestions2",
+ a: a({
+ onClick: () => this.switchToConsole(),
+ }),
+ // NOTE: for <Localized> to parse the markup in the string, the
+ // markup needs to be actual HTML elements
+ span: a({
+ onClick: () => this.switchToDebugger(),
+ }),
+ },
+ p({})
+ ),
+ p(
+ {},
+ Localized(
+ { id: "serviceworker-empty-intro-link" },
+ a({
+ onClick: () => this.openDocumentation(),
+ })
+ )
+ ),
+ p(
+ {},
+ Localized(
+ { id: "serviceworker-empty-suggestions-aboutdebugging2" },
+ a({
+ className: "js-trusted-link",
+ onClick: () => this.openAboutDebugging(),
+ })
+ )
+ )
+ )
+ );
+ }
+}
+
+// Exports
+module.exports = RegistrationListEmpty;
diff --git a/devtools/client/application/src/components/service-workers/Worker.css b/devtools/client/application/src/components/service-workers/Worker.css
new file mode 100644
index 0000000000..e44b49ef6b
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/Worker.css
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /*
+ * The current layout of a service worker item is
+ *
+ * +------------+------------------------------+
+ * | Worker | script_name |
+ * | Icon |------------------------------|
+ * | | status start_button |
+ * +------------+------------------------------+
+ */
+
+.worker {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-template-areas: "icon source"
+ "icon misc";
+ column-gap: calc(var(--base-unit) * 2);
+ row-gap: var(--base-unit);
+
+ line-height: calc(var(--base-unit) * 4);
+ font-size: var(--body-10-font-size);
+}
+
+.worker__icon {
+ grid-area: icon;
+}
+
+.worker__icon-image {
+ width: calc(var(--base-unit) * 4);
+ height: calc(var(--base-unit) * 4);
+}
+
+.worker__source {
+ grid-area: source;
+ user-select: text;
+}
+
+.worker__misc {
+ grid-area: misc;
+}
+
+.worker__status {
+ text-transform: capitalize;
+ --status-bg-color: transparent;
+ --status-border-color: transparent;
+}
+
+.worker__status::before {
+ content: "";
+ margin-inline-end: var(--base-unit);
+ width: calc(var(--base-unit) * 2);
+ height: calc(var(--base-unit) * 2);
+ display: inline-block;
+ background: var(--status-bg-color);
+ border: 1px solid var(--status-border-color);
+ border-radius: 100%;
+}
+
+.worker__status--active {
+ --status-bg-color: var(--green-60);
+ --status-border-color: var(--green-60);
+}
+
+.worker__status--waiting {
+ --status-bg-color: var(--theme-text-color-alt);
+ --status-border-color: var(--theme-text-color-alt);
+}
+
+.worker__status--installing, .worker__status--default {
+ --status-bg-color: transparent;
+ --status-border-color: var(--theme-text-color-alt);
+}
diff --git a/devtools/client/application/src/components/service-workers/Worker.js b/devtools/client/application/src/components/service-workers/Worker.js
new file mode 100644
index 0000000000..bc95e084a9
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/Worker.js
@@ -0,0 +1,225 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ a,
+ img,
+ p,
+ section,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ getUnicodeUrlPath,
+} = require("resource://devtools/client/shared/unicode-url.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+const {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const {
+ services,
+} = require("resource://devtools/client/application/src/modules/application-services.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const {
+ startWorker,
+} = require("resource://devtools/client/application/src/actions/workers.js");
+
+const UIButton = createFactory(
+ require("resource://devtools/client/application/src/components/ui/UIButton.js")
+);
+
+/**
+ * This component is dedicated to display a worker, more accurately a service worker, in
+ * the list of workers displayed in the application panel. It displays information about
+ * the worker as well as action links and buttons to interact with the worker (e.g. debug,
+ * unregister, update etc...).
+ */
+class Worker extends PureComponent {
+ static get propTypes() {
+ return {
+ isDebugEnabled: PropTypes.bool.isRequired,
+ worker: PropTypes.shape(Types.worker).isRequired,
+ // this prop get automatically injected via `connect`
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.debug = this.debug.bind(this);
+ this.viewSource = this.viewSource.bind(this);
+ this.start = this.start.bind(this);
+ }
+
+ debug() {
+ if (!this.isRunning()) {
+ console.log("Service workers cannot be debugged if they are not running");
+ return;
+ }
+
+ services.openWorkerInDebugger(this.props.worker.workerDescriptorFront);
+ }
+
+ viewSource() {
+ if (!this.isRunning()) {
+ console.log(
+ "Service workers cannot be inspected if they are not running"
+ );
+ return;
+ }
+
+ services.viewWorkerSource(this.props.worker.workerDescriptorFront);
+ }
+
+ start() {
+ if (!this.isActive() || this.isRunning()) {
+ console.log("Running or inactive service workers cannot be started");
+ return;
+ }
+
+ this.props.dispatch(startWorker(this.props.worker));
+ }
+
+ isRunning() {
+ // We know the worker is running if it has a worker actor.
+ return !!this.props.worker.workerDescriptorFront;
+ }
+
+ isActive() {
+ return this.props.worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED;
+ }
+
+ getLocalizedStatus() {
+ if (this.isActive() && this.isRunning()) {
+ return l10n.getString("serviceworker-worker-status-running");
+ } else if (this.isActive()) {
+ return l10n.getString("serviceworker-worker-status-stopped");
+ }
+ // NOTE: this is already localized by the service worker front
+ // (strings are in debugger.properties)
+ return this.props.worker.stateText;
+ }
+
+ getClassNameForStatus(baseClass) {
+ const { state } = this.props.worker;
+
+ switch (state) {
+ case Ci.nsIServiceWorkerInfo.STATE_PARSED:
+ case Ci.nsIServiceWorkerInfo.STATE_INSTALLING:
+ return "worker__status--installing";
+ case Ci.nsIServiceWorkerInfo.STATE_INSTALLED:
+ case Ci.nsIServiceWorkerInfo.STATE_ACTIVATING:
+ return "worker__status--waiting";
+ case Ci.nsIServiceWorkerInfo.STATE_ACTIVATED:
+ return "worker__status--active";
+ }
+
+ return "worker__status--default";
+ }
+
+ formatSource(source) {
+ const parts = source.split("/");
+ return getUnicodeUrlPath(parts[parts.length - 1]);
+ }
+
+ renderInspectLink(url) {
+ // avoid rendering the inspect link if sw is not running
+ const isDisabled = !this.isRunning();
+ // view source instead of debugging when debugging sw is not available
+ const callbackFn = this.props.isDebugEnabled ? this.debug : this.viewSource;
+
+ const sourceUrl = span(
+ { className: "js-source-url" },
+ this.formatSource(url)
+ );
+
+ return isDisabled
+ ? sourceUrl
+ : a(
+ {
+ onClick: callbackFn,
+ title: url,
+ href: "#",
+ className: "js-inspect-link",
+ },
+ sourceUrl,
+ "\u00A0", // &nbsp;
+ Localized(
+ {
+ id: "serviceworker-worker-inspect-icon",
+ attrs: {
+ alt: true,
+ },
+ },
+ img({
+ src: "chrome://devtools/skin/images/application-debug.svg",
+ })
+ )
+ );
+ }
+
+ renderStartButton() {
+ // avoid rendering the button at all for workers that are either running,
+ // or in a state that prevents them from starting (like waiting)
+ if (this.isRunning() || !this.isActive()) {
+ return null;
+ }
+
+ return Localized(
+ { id: "serviceworker-worker-start3" },
+ UIButton({
+ onClick: this.start,
+ className: `js-start-button`,
+ size: "micro",
+ })
+ );
+ }
+
+ render() {
+ const { worker } = this.props;
+ const statusText = this.getLocalizedStatus();
+ const statusClassName = this.getClassNameForStatus();
+
+ return section(
+ { className: "worker js-sw-worker" },
+ p(
+ { className: "worker__icon" },
+ img({
+ className: "worker__icon-image",
+ src: "chrome://devtools/skin/images/debugging-workers.svg",
+ })
+ ),
+ p({ className: "worker__source" }, this.renderInspectLink(worker.url)),
+ p(
+ { className: "worker__misc" },
+ span(
+ { className: `js-worker-status worker__status ${statusClassName}` },
+ statusText
+ ),
+ " ",
+ this.renderStartButton()
+ )
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+module.exports = connect(mapDispatchToProps)(Worker);
diff --git a/devtools/client/application/src/components/service-workers/WorkersPage.js b/devtools/client/application/src/components/service-workers/WorkersPage.js
new file mode 100644
index 0000000000..a44dd292f8
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/WorkersPage.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ section,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const RegistrationList = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/RegistrationList.js")
+);
+const RegistrationListEmpty = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/RegistrationListEmpty.js")
+);
+
+class WorkersPage extends PureComponent {
+ static get propTypes() {
+ return {
+ // mapped from state
+ canDebugWorkers: PropTypes.bool.isRequired,
+ domain: PropTypes.string.isRequired,
+ registrations: Types.registrationArray.isRequired,
+ };
+ }
+
+ render() {
+ const { canDebugWorkers, domain, registrations } = this.props;
+
+ // Filter out workers from other domains
+ const domainWorkers = registrations.filter(
+ x => !!x.workers.length && new URL(x.workers[0].url).hostname === domain
+ );
+ const isListEmpty = domainWorkers.length === 0;
+
+ return section(
+ {
+ className: `app-page js-service-workers-page ${
+ isListEmpty ? "app-page--empty" : ""
+ }`,
+ },
+ isListEmpty
+ ? RegistrationListEmpty({})
+ : RegistrationList({
+ canDebugWorkers,
+ registrations: domainWorkers,
+ })
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ canDebugWorkers: state.workers.canDebugWorkers,
+ domain: state.page.domain,
+ registrations: state.workers.list,
+ };
+}
+
+// Exports
+module.exports = connect(mapStateToProps)(WorkersPage);
diff --git a/devtools/client/application/src/components/service-workers/moz.build b/devtools/client/application/src/components/service-workers/moz.build
new file mode 100644
index 0000000000..f9704b9df8
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "Registration.js",
+ "RegistrationList.js",
+ "RegistrationListEmpty.js",
+ "Worker.js",
+ "WorkersPage.js",
+)
diff --git a/devtools/client/application/src/components/ui/UIButton.css b/devtools/client/application/src/components/ui/UIButton.css
new file mode 100644
index 0000000000..2d614e09b0
--- /dev/null
+++ b/devtools/client/application/src/components/ui/UIButton.css
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* these styles com from Photon. Keep in mind that the "default" style is not used
+ in panels, and we should use the "micro" instead for default, stand-alone buttons. */
+
+:root.theme-light {
+ --button-text-color: var(--grey-90);
+ --button-text-hover-color: var(--grey-90);
+ --button-text-pressed-color: var(--grey-90);
+ --button-background-color: var(--grey-90-a10);
+ --button-background-hover-color: var(--grey-90-a20);
+ --button-background-pressed-color: var(--grey-90-a30);
+}
+
+:root.theme-dark {
+ --button-text-color: var(--grey-40);
+ --button-text-hover-color: var(--grey-30);
+ --button-text-pressed-color: var(--grey-30);
+ --button-background-color: var(--grey-10-a20);
+ --button-background-hover-color: var(--grey-10-a25);
+ --button-background-pressed-color: var(--grey-10-a30);
+}
+
+.ui-button {
+ appearance: none;
+ transition: background-color 0.05s ease-in-out;
+
+ margin: 0;
+ height: calc(var(--base-unit) * 6);
+ padding-inline-start: calc(2 * var(--base-unit));
+ padding-inline-end: calc(2 * var(--base-unit));
+ border: none;
+ border-radius: calc(var(--base-unit) / 2);
+
+ color: var(--button-text-color);
+ background: var(--button-background-color);
+ font-size: var(--caption-10-font-size);
+}
+
+.ui-button:-moz-focusring {
+ outline: none;
+}
+.ui-button::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+.ui-button:enabled:hover {
+ background: var(--button-background-hover-color);
+ color: var(--button-text-hover-color);
+}
+
+.ui-button:enabled:active {
+ background: var(--button-background-pressed-color);
+ color: var(--button-text-pressed-color);
+}
+
+.ui-button:focus {
+ box-shadow: 0 0 0 1px var(--blue-50) inset,
+ 0 0 0 1px var(--blue-50),
+ 0 0 0 4px var(--blue-50-a30);
+}
+
+.ui-button:disabled {
+ opacity: 0.4;
+}
+
+/* Note: this "micro" variant here is not the same as the "micro" variant
+ in Photon docs (since we are using that one for our default size) */
+.ui-button--micro {
+ height: auto;
+ padding: calc(var(--base-unit) * 0.5) var(--base-unit);
+}
diff --git a/devtools/client/application/src/components/ui/UIButton.js b/devtools/client/application/src/components/ui/UIButton.js
new file mode 100644
index 0000000000..bc3f297d85
--- /dev/null
+++ b/devtools/client/application/src/components/ui/UIButton.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ button,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+class UIButton extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ disabled: PropTypes.bool,
+ onClick: PropTypes.func,
+ size: PropTypes.oneOf(["micro"]),
+ };
+ }
+
+ render() {
+ const { className, disabled, onClick, size } = this.props;
+ const sizeClass = size ? `ui-button--${size}` : "";
+
+ return button(
+ {
+ className: `ui-button ${className || ""} ${sizeClass}`,
+ onClick,
+ disabled,
+ },
+ this.props.children
+ );
+ }
+}
+
+module.exports = UIButton;
diff --git a/devtools/client/application/src/components/ui/moz.build b/devtools/client/application/src/components/ui/moz.build
new file mode 100644
index 0000000000..f62f66d310
--- /dev/null
+++ b/devtools/client/application/src/components/ui/moz.build
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "UIButton.js",
+)
diff --git a/devtools/client/application/src/constants.js b/devtools/client/application/src/constants.js
new file mode 100644
index 0000000000..f7e34082a3
--- /dev/null
+++ b/devtools/client/application/src/constants.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const actionTypes = {
+ // manifest substate
+ FETCH_MANIFEST_FAILURE: "FETCH_MANIFEST_FAILURE",
+ FETCH_MANIFEST_START: "FETCH_MANIFEST_START",
+ FETCH_MANIFEST_SUCCESS: "FETCH_MANIFEST_SUCCESS",
+ RESET_MANIFEST: "RESET_MANIFEST",
+ // page substate
+ UPDATE_DOMAIN: "UPDATE_DOMAIN",
+ // ui substate
+ UPDATE_SELECTED_PAGE: "UPDATE_SELECTED_PAGE",
+ // workers substate
+ START_WORKER: "START_WORKER",
+ UNREGISTER_WORKER: "UNREGISTER_WORKER",
+ UPDATE_CAN_DEBUG_WORKERS: "UPDATE_CAN_DEBUG_WORKERS",
+ UPDATE_WORKERS: "UPDATE_WORKERS",
+};
+
+// NOTE: these const values are used as part of CSS selectors - be mindful of the characters used
+const PAGE_TYPES = {
+ MANIFEST: "manifest",
+ SERVICE_WORKERS: "service-workers",
+};
+
+const DEFAULT_PAGE = PAGE_TYPES.SERVICE_WORKERS;
+
+const MANIFEST_CATEGORIES = {
+ IDENTITY: "identity",
+ PRESENTATION: "presentation",
+ ICONS: "icons",
+};
+
+const MANIFEST_MEMBER_VALUE_TYPES = {
+ COLOR: "color",
+ ICON: "icon",
+ STRING: "string",
+ URL: "url",
+};
+
+const MANIFEST_ISSUE_LEVELS = {
+ ERROR: "error",
+ WARNING: "warning",
+};
+
+// flatten constants
+module.exports = Object.assign(
+ {},
+ {
+ DEFAULT_PAGE,
+ PAGE_TYPES,
+ MANIFEST_CATEGORIES,
+ MANIFEST_ISSUE_LEVELS,
+ MANIFEST_MEMBER_VALUE_TYPES,
+ },
+ actionTypes
+);
diff --git a/devtools/client/application/src/create-store.js b/devtools/client/application/src/create-store.js
new file mode 100644
index 0000000000..7c89adece3
--- /dev/null
+++ b/devtools/client/application/src/create-store.js
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ thunk,
+} = require("resource://devtools/client/shared/redux/middleware/thunk.js");
+const eventTelemetryMiddleware = require("resource://devtools/client/application/src/middleware/event-telemetry.js");
+
+const {
+ applyMiddleware,
+ createStore,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+
+// Reducers
+
+const rootReducer = require("resource://devtools/client/application/src/reducers/index.js");
+const {
+ ManifestState,
+} = require("resource://devtools/client/application/src/reducers/manifest-state.js");
+const {
+ WorkersState,
+} = require("resource://devtools/client/application/src/reducers/workers-state.js");
+const {
+ PageState,
+} = require("resource://devtools/client/application/src/reducers/page-state.js");
+const {
+ UiState,
+} = require("resource://devtools/client/application/src/reducers/ui-state.js");
+
+function configureStore(telemetry) {
+ // Prepare initial state.
+ const initialState = {
+ manifest: new ManifestState(),
+ page: new PageState(),
+ ui: new UiState(),
+ workers: new WorkersState(),
+ };
+
+ const middleware = applyMiddleware(
+ thunk(),
+ eventTelemetryMiddleware(telemetry)
+ );
+
+ return createStore(rootReducer, initialState, middleware);
+}
+
+exports.configureStore = configureStore;
diff --git a/devtools/client/application/src/middleware/event-telemetry.js b/devtools/client/application/src/middleware/event-telemetry.js
new file mode 100644
index 0000000000..60129d2bde
--- /dev/null
+++ b/devtools/client/application/src/middleware/event-telemetry.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ START_WORKER,
+ UNREGISTER_WORKER,
+ UPDATE_SELECTED_PAGE,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function eventTelemetryMiddleware(telemetry) {
+ function recordEvent(method, details = {}) {
+ telemetry.recordEvent(method, "application", null, details);
+ }
+
+ return store => next => action => {
+ switch (action.type) {
+ // ui telemetry
+ case UPDATE_SELECTED_PAGE:
+ recordEvent("select_page", { page_type: action.selectedPage });
+ break;
+ // service-worker related telemetry
+ case UNREGISTER_WORKER:
+ recordEvent("unregister_worker");
+ break;
+ case START_WORKER:
+ recordEvent("start_worker");
+ break;
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = eventTelemetryMiddleware;
diff --git a/devtools/client/application/src/middleware/moz.build b/devtools/client/application/src/middleware/moz.build
new file mode 100644
index 0000000000..5041f3ca13
--- /dev/null
+++ b/devtools/client/application/src/middleware/moz.build
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "event-telemetry.js",
+)
diff --git a/devtools/client/application/src/modules/application-services.js b/devtools/client/application/src/modules/application-services.js
new file mode 100644
index 0000000000..9188b07f07
--- /dev/null
+++ b/devtools/client/application/src/modules/application-services.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// keyword to use in telemetry, as `reason` parameter
+const REASON = "application";
+
+class ManifestDevToolsError extends Error {
+ constructor(...params) {
+ super(...params);
+
+ this.name = "ManifestDevToolsError";
+ }
+}
+
+class ApplicationServices {
+ init(toolbox) {
+ this._toolbox = toolbox;
+
+ this.features = {
+ doesDebuggerSupportWorkers: Services.prefs.getBoolPref(
+ "devtools.debugger.features.windowless-service-workers",
+ false
+ ),
+ };
+ }
+
+ selectTool(toolId) {
+ this._assertInit();
+ return this._toolbox.selectTool(toolId, REASON);
+ }
+
+ async openWorkerInDebugger(workerDescriptorFront) {
+ const debuggerPanel = await this.selectTool("jsdebugger");
+ debuggerPanel.selectServiceWorker(workerDescriptorFront);
+ }
+
+ async viewWorkerSource(workerDescriptorFront) {
+ // NOTE: this falls back to view-source: if the source can't be inspected
+ // within the debugger.
+ this._toolbox.viewSourceInDebugger(
+ workerDescriptorFront.url,
+ 1,
+ 1,
+ null,
+ REASON
+ );
+ }
+
+ async fetchManifest() {
+ let response;
+
+ try {
+ this._assertInit();
+ const manifestFront = await this._toolbox.target.getFront("manifest");
+ response = await manifestFront.fetchCanonicalManifest();
+ } catch (error) {
+ throw new ManifestDevToolsError(
+ error.message,
+ error.fileName,
+ error.lineNumber
+ );
+ }
+
+ if (response.errorMessage) {
+ throw new Error(response.errorMessage);
+ }
+
+ return response.manifest;
+ }
+
+ _assertInit() {
+ if (!this._toolbox) {
+ throw new Error("Services singleton has not been initialized");
+ }
+ }
+}
+
+module.exports = {
+ ManifestDevToolsError,
+ // exports a singleton, which will be used across all application panel modules
+ services: new ApplicationServices(),
+};
diff --git a/devtools/client/application/src/modules/l10n.js b/devtools/client/application/src/modules/l10n.js
new file mode 100644
index 0000000000..21e300cb38
--- /dev/null
+++ b/devtools/client/application/src/modules/l10n.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ FluentL10n,
+} = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js");
+
+// exports a singleton, which will be used across all application panel modules
+exports.l10n = new FluentL10n();
diff --git a/devtools/client/application/src/modules/moz.build b/devtools/client/application/src/modules/moz.build
new file mode 100644
index 0000000000..778345fb1f
--- /dev/null
+++ b/devtools/client/application/src/modules/moz.build
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "application-services.js",
+ "l10n.js",
+)
diff --git a/devtools/client/application/src/moz.build b/devtools/client/application/src/moz.build
new file mode 100644
index 0000000000..58e6f92857
--- /dev/null
+++ b/devtools/client/application/src/moz.build
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "actions",
+ "components",
+ "middleware",
+ "modules",
+ "reducers",
+ "types",
+]
+
+DevToolsModules(
+ "constants.js",
+ "create-store.js",
+)
diff --git a/devtools/client/application/src/reducers/index.js b/devtools/client/application/src/reducers/index.js
new file mode 100644
index 0000000000..8b93290d9d
--- /dev/null
+++ b/devtools/client/application/src/reducers/index.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ combineReducers,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ workersReducer,
+} = require("resource://devtools/client/application/src/reducers/workers-state.js");
+const {
+ pageReducer,
+} = require("resource://devtools/client/application/src/reducers/page-state.js");
+const {
+ uiReducer,
+} = require("resource://devtools/client/application/src/reducers/ui-state.js");
+const {
+ manifestReducer,
+} = require("resource://devtools/client/application/src/reducers/manifest-state.js");
+
+module.exports = combineReducers({
+ manifest: manifestReducer,
+ page: pageReducer,
+ workers: workersReducer,
+ ui: uiReducer,
+});
diff --git a/devtools/client/application/src/reducers/manifest-state.js b/devtools/client/application/src/reducers/manifest-state.js
new file mode 100644
index 0000000000..61a2fa6759
--- /dev/null
+++ b/devtools/client/application/src/reducers/manifest-state.js
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ MANIFEST_CATEGORIES,
+ MANIFEST_ISSUE_LEVELS,
+ MANIFEST_MEMBER_VALUE_TYPES,
+ FETCH_MANIFEST_FAILURE,
+ FETCH_MANIFEST_START,
+ FETCH_MANIFEST_SUCCESS,
+ RESET_MANIFEST,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function _processRawManifestIcons(rawIcons) {
+ // NOTE: about `rawIcons` array we are getting from platform:
+ // - Icons that do not comform to the spec are filtered out
+ // - We will always get a `src`
+ // - We will always get `purpose` with a value (default is `["any"]`)
+ // - `sizes` may be undefined
+ // - `type` may be undefined
+ return rawIcons.map(icon => {
+ return {
+ key: {
+ sizes: Array.isArray(icon.sizes) ? icon.sizes.join(" ") : icon.sizes,
+ contentType: icon.type,
+ },
+ value: {
+ src: icon.src,
+ purpose: icon.purpose.join(" "),
+ },
+ type: MANIFEST_MEMBER_VALUE_TYPES.ICON,
+ };
+ });
+}
+
+function _processRawManifestMembers(rawManifest) {
+ function getCategoryForMember(key) {
+ switch (key) {
+ case "name":
+ case "short_name":
+ return MANIFEST_CATEGORIES.IDENTITY;
+ default:
+ return MANIFEST_CATEGORIES.PRESENTATION;
+ }
+ }
+
+ function getValueTypeForMember(key) {
+ switch (key) {
+ case "start_url":
+ case "scope":
+ return MANIFEST_MEMBER_VALUE_TYPES.URL;
+ case "theme_color":
+ case "background_color":
+ return MANIFEST_MEMBER_VALUE_TYPES.COLOR;
+ default:
+ return MANIFEST_MEMBER_VALUE_TYPES.STRING;
+ }
+ }
+
+ const res = {
+ [MANIFEST_CATEGORIES.IDENTITY]: [],
+ [MANIFEST_CATEGORIES.PRESENTATION]: [],
+ };
+
+ // filter out extra metadata members (those with moz_ prefix) and icons
+ const rawMembers = Object.entries(rawManifest).filter(
+ ([key, value]) => !key.startsWith("moz_") && !(key === "icons")
+ );
+
+ for (const [key, value] of rawMembers) {
+ const category = getCategoryForMember(key);
+ const type = getValueTypeForMember(key);
+ res[category].push({ key, value, type });
+ }
+
+ return res;
+}
+
+function _processRawManifestIssues(issues) {
+ return issues.map(x => {
+ return {
+ level: x.warn
+ ? MANIFEST_ISSUE_LEVELS.WARNING
+ : MANIFEST_ISSUE_LEVELS.ERROR,
+ message: x.warn || x.error,
+ type: x.type || null,
+ };
+ });
+}
+
+function _processRawManifest(rawManifest) {
+ const res = {
+ url: rawManifest.moz_manifest_url,
+ };
+
+ // group manifest members by category
+ Object.assign(res, _processRawManifestMembers(rawManifest));
+ // process icons
+ res.icons = _processRawManifestIcons(rawManifest.icons || []);
+ // process error messages
+ res.validation = _processRawManifestIssues(rawManifest.moz_validation || []);
+
+ return res;
+}
+
+function ManifestState() {
+ return {
+ errorMessage: "",
+ isLoading: false,
+ manifest: undefined,
+ };
+}
+
+function manifestReducer(state = ManifestState(), action) {
+ switch (action.type) {
+ case FETCH_MANIFEST_START:
+ return Object.assign({}, state, {
+ isLoading: true,
+ mustLoadManifest: false,
+ });
+
+ case FETCH_MANIFEST_FAILURE:
+ const { error } = action;
+ // If we add a redux middleware to log errors, we should move the
+ // console.error below there.
+ console.error(error);
+ return Object.assign({}, state, {
+ errorMessage: error,
+ isLoading: false,
+ manifest: null,
+ });
+
+ case FETCH_MANIFEST_SUCCESS:
+ // NOTE: we don't get an error when the page does not have a manifest,
+ // but a `null` value there.
+ const { manifest } = action;
+ return Object.assign({}, state, {
+ errorMessage: "",
+ isLoading: false,
+ manifest: manifest ? _processRawManifest(manifest) : null,
+ });
+
+ case RESET_MANIFEST:
+ const defaultState = ManifestState();
+ return defaultState;
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ ManifestState,
+ manifestReducer,
+};
diff --git a/devtools/client/application/src/reducers/moz.build b/devtools/client/application/src/reducers/moz.build
new file mode 100644
index 0000000000..752b27a685
--- /dev/null
+++ b/devtools/client/application/src/reducers/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "index.js",
+ "manifest-state.js",
+ "page-state.js",
+ "ui-state.js",
+ "workers-state.js",
+)
diff --git a/devtools/client/application/src/reducers/page-state.js b/devtools/client/application/src/reducers/page-state.js
new file mode 100644
index 0000000000..ef3b6c970d
--- /dev/null
+++ b/devtools/client/application/src/reducers/page-state.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ UPDATE_DOMAIN,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function PageState() {
+ return {
+ // Domain
+ domain: null,
+ };
+}
+
+function getDomainFromUrl(url) {
+ return new URL(url).hostname;
+}
+
+function pageReducer(state = PageState(), action) {
+ switch (action.type) {
+ case UPDATE_DOMAIN: {
+ const { url } = action;
+ return {
+ domain: getDomainFromUrl(url),
+ };
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ PageState,
+ pageReducer,
+};
diff --git a/devtools/client/application/src/reducers/ui-state.js b/devtools/client/application/src/reducers/ui-state.js
new file mode 100644
index 0000000000..81a57c8086
--- /dev/null
+++ b/devtools/client/application/src/reducers/ui-state.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ DEFAULT_PAGE,
+ UPDATE_SELECTED_PAGE,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function UiState() {
+ return {
+ selectedPage: DEFAULT_PAGE,
+ };
+}
+
+function uiReducer(state = UiState(), action) {
+ switch (action.type) {
+ case UPDATE_SELECTED_PAGE:
+ return Object.assign({}, state, { selectedPage: action.selectedPage });
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ UiState,
+ uiReducer,
+};
diff --git a/devtools/client/application/src/reducers/workers-state.js b/devtools/client/application/src/reducers/workers-state.js
new file mode 100644
index 0000000000..004c25ddfa
--- /dev/null
+++ b/devtools/client/application/src/reducers/workers-state.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ START_WORKER,
+ UNREGISTER_WORKER,
+ UPDATE_CAN_DEBUG_WORKERS,
+ UPDATE_WORKERS,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function WorkersState() {
+ return {
+ // Array of all service worker registrations
+ list: [],
+ canDebugWorkers: false,
+ };
+}
+
+function buildWorkerDataFromFronts({ registration, workers }) {
+ return {
+ id: registration.id,
+ lastUpdateTime: registration.lastUpdateTime,
+ registrationFront: registration,
+ scope: registration.scope,
+ workers: workers.map(worker => ({
+ id: worker.id,
+ url: worker.url,
+ state: worker.state,
+ stateText: worker.stateText,
+ registrationFront: registration,
+ workerDescriptorFront: worker.workerDescriptorFront,
+ })),
+ };
+}
+
+function workersReducer(state = WorkersState(), action) {
+ switch (action.type) {
+ case UPDATE_CAN_DEBUG_WORKERS: {
+ return Object.assign({}, state, {
+ canDebugWorkers: action.canDebugWorkers,
+ });
+ }
+ case UPDATE_WORKERS: {
+ const { workers } = action;
+ return Object.assign({}, state, {
+ list: workers.map(buildWorkerDataFromFronts).flat(),
+ });
+ }
+ // these actions don't change the state, but get picked up by the
+ // telemetry middleware
+ case START_WORKER:
+ case UNREGISTER_WORKER:
+ return state;
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ WorkersState,
+ workersReducer,
+};
diff --git a/devtools/client/application/src/types/index.js b/devtools/client/application/src/types/index.js
new file mode 100644
index 0000000000..bf15f187a6
--- /dev/null
+++ b/devtools/client/application/src/types/index.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const manifestTypes = require("resource://devtools/client/application/src/types/manifest.js");
+const routingTypes = require("resource://devtools/client/application/src/types/routing.js");
+const workersTypes = require("resource://devtools/client/application/src/types/service-workers.js");
+
+module.exports = Object.assign(
+ {},
+ {
+ ...manifestTypes,
+ ...routingTypes,
+ ...workersTypes,
+ }
+);
diff --git a/devtools/client/application/src/types/manifest.js b/devtools/client/application/src/types/manifest.js
new file mode 100644
index 0000000000..7f49522a25
--- /dev/null
+++ b/devtools/client/application/src/types/manifest.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ MANIFEST_ISSUE_LEVELS,
+} = require("resource://devtools/client/application/src/constants.js");
+const {
+ MANIFEST_MEMBER_VALUE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const manifestIssue = {
+ level: PropTypes.oneOf(Object.values(MANIFEST_ISSUE_LEVELS)).isRequired,
+ message: PropTypes.string.isRequired,
+ // NOTE: we are currently ignoring the 'type' field that platform adds to errors
+};
+
+const manifestIssueArray = PropTypes.arrayOf(PropTypes.shape(manifestIssue));
+
+const manifestItemColor = {
+ label: PropTypes.string.isRequired,
+ value: PropTypes.string,
+};
+
+const manifestItemIcon = {
+ label: PropTypes.shape({
+ contentType: PropTypes.string,
+ sizes: PropTypes.string,
+ }).isRequired,
+ value: PropTypes.shape({
+ src: PropTypes.string.isRequired,
+ purpose: PropTypes.string.isRequired,
+ }).isRequired,
+};
+
+const manifestItemUrl = {
+ label: PropTypes.string.isRequired,
+ value: PropTypes.string,
+};
+
+const manifestMemberColor = {
+ key: manifestItemColor.label,
+ value: manifestItemColor.value,
+ type: PropTypes.oneOf([MANIFEST_MEMBER_VALUE_TYPES.COLOR]),
+};
+
+const manifestMemberIcon = {
+ key: manifestItemIcon.label,
+ value: manifestItemIcon.value,
+ type: PropTypes.oneOf([MANIFEST_MEMBER_VALUE_TYPES.ICON]),
+};
+
+const manifestMemberString = {
+ key: PropTypes.string.isRequired,
+ value: PropTypes.string,
+ type: PropTypes.oneOf([MANIFEST_MEMBER_VALUE_TYPES.STRING]),
+};
+
+const manifest = {
+ // members
+ identity: PropTypes.arrayOf(PropTypes.shape(manifestMemberString)).isRequired,
+ presentation: PropTypes.arrayOf(
+ PropTypes.oneOfType([
+ PropTypes.shape(manifestMemberColor),
+ PropTypes.shape(manifestMemberString),
+ ])
+ ).isRequired,
+ icons: PropTypes.arrayOf(PropTypes.shape(manifestMemberIcon)).isRequired,
+ // validation issues
+ validation: manifestIssueArray.isRequired,
+ // misc
+ url: PropTypes.string.isRequired,
+};
+
+module.exports = {
+ // full manifest
+ manifest,
+ // specific manifest items
+ manifestItemColor,
+ manifestItemIcon,
+ manifestItemUrl,
+ // manifest issues
+ manifestIssue,
+ manifestIssueArray,
+};
diff --git a/devtools/client/application/src/types/moz.build b/devtools/client/application/src/types/moz.build
new file mode 100644
index 0000000000..c8161f448d
--- /dev/null
+++ b/devtools/client/application/src/types/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "index.js",
+ "manifest.js",
+ "routing.js",
+ "service-workers.js",
+)
diff --git a/devtools/client/application/src/types/routing.js b/devtools/client/application/src/types/routing.js
new file mode 100644
index 0000000000..a1d922ab3d
--- /dev/null
+++ b/devtools/client/application/src/types/routing.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const page = PropTypes.oneOf(Object.values(PAGE_TYPES));
+
+module.exports = {
+ page,
+};
diff --git a/devtools/client/application/src/types/service-workers.js b/devtools/client/application/src/types/service-workers.js
new file mode 100644
index 0000000000..bf913fb264
--- /dev/null
+++ b/devtools/client/application/src/types/service-workers.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const worker = {
+ id: PropTypes.string.isRequired,
+ state: PropTypes.number.isRequired,
+ stateText: PropTypes.string.isRequired,
+ url: PropTypes.string.isRequired,
+ workerDescriptorFront: PropTypes.object,
+ registrationFront: PropTypes.object,
+};
+
+const workerArray = PropTypes.arrayOf(PropTypes.shape(worker));
+
+const registration = {
+ id: PropTypes.string.isRequired,
+ lastUpdateTime: PropTypes.number,
+ registrationFront: PropTypes.object.isRequired,
+ scope: PropTypes.string.isRequired,
+ workers: workerArray.isRequired,
+};
+
+const registrationArray = PropTypes.arrayOf(PropTypes.shape(registration));
+
+module.exports = {
+ registration,
+ registrationArray,
+ worker,
+ workerArray,
+};
diff --git a/devtools/client/application/test/browser/browser.toml b/devtools/client/application/test/browser/browser.toml
new file mode 100644
index 0000000000..5dea736ed3
--- /dev/null
+++ b/devtools/client/application/test/browser/browser.toml
@@ -0,0 +1,150 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "head.js",
+ "resources/manifest/icon.svg",
+ "resources/manifest/load-fail.html",
+ "resources/manifest/load-no-manifest.html",
+ "resources/manifest/load-ok-icons.html",
+ "resources/manifest/load-ok-json-error.html",
+ "resources/manifest/load-ok-manifest-link.html",
+ "resources/manifest/load-ok-warnings.html",
+ "resources/manifest/load-ok.html",
+ "resources/manifest/manifest.json",
+ "resources/service-workers/controlled-install-sw.js",
+ "resources/service-workers/controlled-install.html",
+ "resources/service-workers/debug-sw.js",
+ "resources/service-workers/debug.html",
+ "resources/service-workers/dynamic-registration.html",
+ "resources/service-workers/empty.html",
+ "resources/service-workers/empty-sw.js",
+ "resources/service-workers/scope-page.html",
+ "resources/service-workers/simple.html",
+ "resources/service-workers/simple-unicode.html",
+ "!/devtools/client/debugger/test/mochitest/shared-head.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+]
+
+["browser_application_panel_debug-service-worker.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "debug", # Bug 1559591
+ "asan", # Bug 1575578
+ "!serviceworker_e10s", # Bug 1588154
+]
+
+["browser_application_panel_list-domain-workers.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["debug"] # Bug 1559591
+
+["browser_application_panel_list-multiple-workers-same-registration.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["debug"] # Bug 1559591
+
+["browser_application_panel_list-several-workers.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["debug"] # Bug 1559591
+
+["browser_application_panel_list-single-worker.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["debug"] # Bug 1559591
+
+["browser_application_panel_list-unicode.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["debug"] # Bug 1559591
+
+["browser_application_panel_list-workers-empty.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_application_panel_manifest-display.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_application_panel_manifest-load.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_application_panel_manifest-open-json.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_application_panel_manifest-reload.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_application_panel_open-links.js"]
+skip-if = ["true"] # Bug 1467256, 1559591
+
+["browser_application_panel_sidebar.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_application_panel_start-service-worker.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "asan", # Bug 1559487
+ "debug", # Bug 1559591
+ "!serviceworker_e10s",
+ "tsan", # Bug 1608640
+]
+
+["browser_application_panel_target-switching.js"]
+https_first_disabled = true
+skip-if = [
+ "os == 'win'", # Bug 1640234
+ "os == 'linux'", # Bug 1640234
+]
+
+["browser_application_panel_telemetry-debug-worker.js"]
+https_first_disabled = true
+skip-if = [
+ "asan", # Bug 1559487
+ "debug", # Bug 1559591
+ "!serviceworker_e10s", # Bug 1608640
+ "os == 'linux' && bits == 64 && !debug", # Bug 1654354
+]
+
+["browser_application_panel_telemetry-select-page.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_application_panel_telemetry-start-worker.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "ccov",
+ "asan", # Bug 1559487
+ "debug", # Bug 1559591
+ "!serviceworker_e10s", # Bug 1608640
+ "tsan", # Bug 1654468
+]
+
+["browser_application_panel_telemetry-unregister-worker.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "asan", # Bug 1559487
+ "debug", # Bug 1559591
+ "!serviceworker_e10s", # Bug 1608640
+]
+
+["browser_application_panel_unregister-worker.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = ["debug"] # Bug 1559591
+
+["browser_application_panel_viewsource-service-worker.js"]
+https_first_disabled = true
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "debug", # Bug 1559591
+ "asan", # Bug 1575578
+ "!serviceworker_e10s", # Bug 1588154
+]
+
+["browser_application_panel_worker-states.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+skip-if = [
+ "asan", # Bug 1559487
+ "debug", # Bug 1559591
+ "!serviceworker_e10s", # Bug 1608640
+]
diff --git a/devtools/client/application/test/browser/browser_application_panel_debug-service-worker.js b/devtools/client/application/test/browser/browser_application_panel_debug-service-worker.js
new file mode 100644
index 0000000000..57dd000edb
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_debug-service-worker.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js",
+ this
+);
+
+const TAB_URL = URL_ROOT + "resources/service-workers/debug.html";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, tab, toolbox, commands } = await openNewTabAndApplicationPanel(
+ TAB_URL
+ );
+
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ const container = getWorkerContainers(doc)[0];
+ info("Wait until the inspect link is displayed");
+ await waitUntil(() => {
+ return container.querySelector(".js-inspect-link");
+ });
+
+ info("Click on the inspect link and wait for debugger to be ready");
+ const debugLink = container.querySelector(".js-inspect-link");
+ debugLink.click();
+ await waitFor(() => toolbox.getPanel("jsdebugger"));
+
+ // add a breakpoint at line 11
+ const debuggerContext = createDebuggerContext(toolbox);
+ await waitForLoadedSource(debuggerContext, "debug-sw.js");
+ await addBreakpoint(debuggerContext, "debug-sw.js", 11);
+
+ // force a pause at the breakpoint
+ info("Invoke fetch, expect the service worker script to pause on line 11");
+ await ContentTask.spawn(tab.linkedBrowser, {}, async function () {
+ content.wrappedJSObject.fetchFromWorker();
+ });
+ await waitForPaused(debuggerContext);
+ const workerScript = findSource(debuggerContext, "debug-sw.js");
+ assertPausedAtSourceAndLine(debuggerContext, workerScript.id, 11);
+ await resume(debuggerContext);
+
+ // remove breakpoint
+ await removeBreakpoint(debuggerContext, workerScript.id, 11);
+
+ await unregisterAllWorkers(commands.client, doc);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_list-domain-workers.js b/devtools/client/application/test/browser/browser_application_panel_list-domain-workers.js
new file mode 100644
index 0000000000..ccb0884d0e
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_list-domain-workers.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the application panel only displays service workers from the
+ * current domain.
+ */
+
+const SIMPLE_URL = URL_ROOT + "resources/service-workers/simple.html";
+const OTHER_URL = SIMPLE_URL.replace("example.com", "test1.example.com");
+const EMPTY_URL = (URL_ROOT + "resources/service-workers/empty.html").replace(
+ "example.com",
+ "test2.example.com"
+);
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, commands, tab } = await openNewTabAndApplicationPanel(
+ SIMPLE_URL
+ );
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ let scopeEl = getWorkerContainers(doc)[0].querySelector(".js-sw-scope");
+ ok(
+ scopeEl.textContent.startsWith("example.com"),
+ "First service worker registration is displayed for the correct domain"
+ );
+
+ info(
+ "Navigate to another page for a different domain with no service worker"
+ );
+
+ await navigateTo(EMPTY_URL);
+ info("Wait until the service worker list is updated");
+ await waitUntil(
+ () => doc.querySelector(".js-registration-list-empty") !== null
+ );
+ ok(
+ true,
+ "No service workers are shown for an empty page in a different domain."
+ );
+
+ info(
+ "Navigate to another page for a different domain with another service worker"
+ );
+ await navigateTo(OTHER_URL);
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ scopeEl = getWorkerContainers(doc)[0].querySelector(".js-sw-scope");
+ ok(
+ scopeEl.textContent.startsWith("test1.example.com"),
+ "Second service worker registration is displayed for the correct domain"
+ );
+
+ await unregisterAllWorkers(commands.client, doc);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_list-multiple-workers-same-registration.js b/devtools/client/application/test/browser/browser_application_panel_list-multiple-workers-same-registration.js
new file mode 100644
index 0000000000..8e117927d1
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_list-multiple-workers-same-registration.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const WORKER1_URL = URL_ROOT + "resources/service-workers/simple.html";
+const WORKER2_URL = URL_ROOT + "resources/service-workers/debug.html";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ await openTabAndWaitForWorker(WORKER1_URL);
+ const { panel, tab, commands } = await openTabAndWaitForWorker(WORKER2_URL);
+
+ const doc = panel.panelWin.document;
+
+ let registrationContainer = getWorkerContainers(doc)[0];
+
+ info("Wait until the unregister button is displayed for the registration");
+ await waitUntil(() => {
+ registrationContainer = getWorkerContainers(doc)[0];
+ return registrationContainer.querySelector(".js-unregister-button");
+ });
+
+ const scopeEl = registrationContainer.querySelector(".js-sw-scope");
+ const expectedScope =
+ "example.com/browser/devtools/client/application/test/" +
+ "browser/resources/service-workers";
+ ok(
+ scopeEl.textContent.startsWith(expectedScope),
+ "Registration has the expected scope"
+ );
+
+ // check the workers data
+ // note that the worker from WORKER2_URL will appear second in the list with
+ // the "installed" state
+ info("Check the workers data for this registration");
+ const workers = registrationContainer.querySelectorAll(".js-sw-worker");
+ is(workers.length, 2, "Registration has two workers");
+ // check url for worker from WORKER1_URL
+ const url1El = workers[0].querySelector(".js-source-url");
+ is(url1El.textContent, "empty-sw.js", "First worker has correct URL");
+ // check url for worker from WORKER2_URL
+ const url2El = workers[1].querySelector(".js-source-url");
+ is(url2El.textContent, "debug-sw.js", "Second worker has correct URL");
+
+ await unregisterAllWorkers(commands.client, doc);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
+
+async function openTabAndWaitForWorker(url) {
+ const { panel, commands, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ return { panel, commands, tab };
+}
diff --git a/devtools/client/application/test/browser/browser_application_panel_list-several-workers.js b/devtools/client/application/test/browser/browser_application_panel_list-several-workers.js
new file mode 100644
index 0000000000..4e1295082e
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_list-several-workers.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the application panel can display several service workers applying to the
+ * same domain.
+ */
+
+const SIMPLE_URL = URL_ROOT + "resources/service-workers/simple.html";
+const OTHER_SCOPE_URL = URL_ROOT + "resources/service-workers/scope-page.html";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, commands, tab } = await openNewTabAndApplicationPanel(
+ SIMPLE_URL
+ );
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ info("Wait until the unregister button is displayed for the service worker");
+ await waitUntil(() =>
+ getWorkerContainers(doc)[0].querySelector(".js-unregister-button")
+ );
+
+ ok(true, "First service worker registration is displayed");
+
+ info(
+ "Navigate to another page for the same domain with another service worker"
+ );
+ await navigateTo(OTHER_SCOPE_URL);
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 2);
+
+ info("Wait until the unregister button is displayed for the service worker");
+ await waitUntil(() =>
+ getWorkerContainers(doc)[1].querySelector(".js-unregister-button")
+ );
+
+ ok(true, "Second service worker registration is displayed");
+
+ await unregisterAllWorkers(commands.client, doc);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_list-single-worker.js b/devtools/client/application/test/browser/browser_application_panel_list-single-worker.js
new file mode 100644
index 0000000000..74cab13bf6
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_list-single-worker.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL =
+ URL_ROOT + "resources/service-workers/dynamic-registration.html";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, tab } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Check for non-existing service worker");
+ const isWorkerListEmpty = !!doc.querySelector(".js-registration-list-empty");
+ ok(isWorkerListEmpty, "No Service Worker displayed");
+
+ info("Register a service worker in the page.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.wrappedJSObject.registerServiceWorker();
+ });
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => !!getWorkerContainers(doc).length);
+
+ let workerContainer = getWorkerContainers(doc)[0];
+
+ info("Wait until the unregister button is displayed for the service worker");
+ await waitUntil(() => {
+ workerContainer = getWorkerContainers(doc)[0];
+ return workerContainer.querySelector(".js-unregister-button");
+ });
+
+ const scopeEl = workerContainer.querySelector(".js-sw-scope");
+ const expectedScope =
+ "example.com/browser/devtools/client/application/test/" +
+ "browser/resources/service-workers";
+ ok(
+ scopeEl.textContent.startsWith(expectedScope),
+ "Service worker has the expected scope"
+ );
+
+ const updatedEl = workerContainer.querySelector(".js-sw-updated");
+ ok(
+ updatedEl.textContent.includes(`${new Date().getFullYear()}`),
+ "Service worker has a last updated time"
+ );
+
+ info("Unregister the service worker");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const registration = await content.wrappedJSObject.sw;
+ registration.unregister();
+ });
+
+ info("Wait until the service worker is removed from the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 0);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_list-unicode.js b/devtools/client/application/test/browser/browser_application_panel_list-unicode.js
new file mode 100644
index 0000000000..c747fa9ae3
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_list-unicode.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = (
+ URL_ROOT + "resources/service-workers/simple-unicode.html"
+).replace("example.com", "xn--hxajbheg2az3al.xn--jxalpdlp");
+
+/**
+ * Check that the application panel displays filenames and URL's in human-readable,
+ * Unicode characters, and not encoded URI's or punycode.
+ */
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ const workerContainer = getWorkerContainers(doc)[0];
+
+ const scopeEl = workerContainer.querySelector(".js-sw-scope");
+ ok(
+ scopeEl.textContent.startsWith(
+ "\u03C0\u03B1\u03C1\u03AC\u03B4\u03B5\u03B9\u03B3\u03BC\u03B1." +
+ "\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE"
+ ),
+ "Service worker has the expected Unicode scope"
+ );
+ const urlEl = workerContainer.querySelector(".js-source-url");
+ ok(
+ urlEl.textContent.endsWith("\u65E5\u672C"),
+ "Service worker has the expected Unicode url"
+ );
+
+ await unregisterAllWorkers(commands.client, doc);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_list-workers-empty.js b/devtools/client/application/test/browser/browser_application_panel_list-workers-empty.js
new file mode 100644
index 0000000000..1bb75d2999
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_list-workers-empty.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the application panel only displays service workers from the
+ * current domain.
+ */
+
+const EMPTY_URL = URL_ROOT + "resources/service-workers/empty.html";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, tab } = await openNewTabAndApplicationPanel(EMPTY_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ await waitUntil(
+ () => doc.querySelector(".js-registration-list-empty") !== null
+ );
+ ok(true, "No service workers are shown for an empty page");
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_manifest-display.js b/devtools/client/application/test/browser/browser_application_panel_manifest-display.js
new file mode 100644
index 0000000000..632c547f42
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_manifest-display.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the manifest is being properly shown
+ */
+
+add_task(async function () {
+ info("Test that we are displaying correctly a valid manifest");
+ const url = URL_ROOT + "resources/manifest/load-ok.html";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest to be displayed");
+ await waitUntil(() => doc.querySelector(".js-manifest") !== null);
+ ok(true, "Manifest is being displayed");
+
+ // assert manifest members are being properly displayed
+ checkManifestMember(doc, "name", "Foo");
+ checkManifestMember(doc, "background_color", "#ff0000");
+
+ Assert.strictEqual(
+ doc.querySelector(".js-manifest-issues"),
+ null,
+ "No validation issues are being displayed"
+ );
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info(
+ "Test that we are displaying correctly a manifest with validation warnings"
+ );
+ const url = URL_ROOT + "resources/manifest/load-ok-warnings.html";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest to be displayed");
+ await waitUntil(() => doc.querySelector(".js-manifest") !== null);
+ ok(true, "Manifest is being displayed");
+
+ // assert manifest members are being properly displayed
+ checkManifestMember(doc, "name", "Foo");
+ checkManifestMember(doc, "background_color", "");
+
+ const issuesEl = doc.querySelector(".js-manifest-issues");
+ Assert.notStrictEqual(issuesEl, null, "Validation issues are displayed");
+
+ const warningEl = [...issuesEl.querySelectorAll(".js-manifest-issue")].find(
+ x => x.textContent.includes("background_color")
+ );
+ Assert.notStrictEqual(
+ warningEl,
+ null,
+ "A warning about background_color is displayed"
+ );
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Test that we are displaying correctly a manifest with JSON errors");
+ const url = URL_ROOT + "resources/manifest/load-ok-json-error.html";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest to be displayed");
+ await waitUntil(() => doc.querySelector(".js-manifest") !== null);
+ ok(true, "Manifest is being displayed");
+
+ const issuesEl = doc.querySelector(".js-manifest-issues");
+ Assert.notStrictEqual(issuesEl, null, "Validation issues are displayed");
+
+ const errorEl = [...issuesEl.querySelectorAll(".js-manifest-issue")].find(x =>
+ x.textContent.includes("JSON")
+ );
+ Assert.notStrictEqual(
+ errorEl,
+ null,
+ "An error about JSON parsing is displayed"
+ );
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Test that we are displaying correctly a manifest with icons");
+ const url = URL_ROOT + "resources/manifest/load-ok-icons.html";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest to be displayed");
+ await waitUntil(() => doc.querySelector(".js-manifest") !== null);
+ ok(true, "Manifest is being displayed");
+
+ // assert manifest icon is being displayed
+ const iconEl = findMemberByLabel(doc, "128x128image/svg");
+ Assert.notStrictEqual(
+ iconEl,
+ null,
+ "Icon label is being displayed with size and image type"
+ );
+ const imgEl = iconEl.querySelector(".js-manifest-item-content img");
+ Assert.notStrictEqual(imgEl, null, "An image is displayed for the icon");
+ is(
+ imgEl.src,
+ URL_ROOT + "resources/manifest/icon.svg",
+ "The icon image has the the icon url as source"
+ );
+ const iconTextContent = iconEl.querySelector(
+ ".js-manifest-item-content"
+ ).textContent;
+ ok(iconTextContent.includes("any"), "Purpose is being displayed");
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
+
+function findMemberByLabel(doc, member) {
+ return [...doc.querySelectorAll(".js-manifest-item")].find(x =>
+ x.querySelector(".js-manifest-item-label").textContent.startsWith(member)
+ );
+}
+
+function checkManifestMember(doc, member, expectedValue) {
+ const itemEl = findMemberByLabel(doc, member);
+ is(
+ itemEl.querySelector(".js-manifest-item-content").textContent,
+ expectedValue,
+ `Manifest member ${member} displays the correct value`
+ );
+}
diff --git a/devtools/client/application/test/browser/browser_application_panel_manifest-load.js b/devtools/client/application/test/browser/browser_application_panel_manifest-load.js
new file mode 100644
index 0000000000..dd89fbc1a6
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_manifest-load.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the application panel fetches a manifest when in the Manifest Page
+ */
+
+add_task(async function () {
+ info("Test that manifest page loads the manifest successfully");
+ const url = URL_ROOT + "resources/manifest/load-ok.html";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest to load");
+ await waitUntil(() => doc.querySelector(".js-manifest") !== null);
+ ok(true, "Manifest loaded successfully");
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Test that manifest page shows an error when failing to load");
+ const url = URL_ROOT + "resources/manifest/load-fail.html";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest to fail to load");
+ await waitUntil(
+ () => doc.querySelector(".js-manifest-loaded-error") !== null
+ );
+ ok(true, "Manifest page displays loading error");
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Test that manifest page shows a message when there is no manifest");
+ const url = URL_ROOT + "resources/manifest/load-no-manifest.html";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the 'no manifest' message to appear");
+ await waitUntil(() => doc.querySelector(".js-manifest-empty") !== null);
+ ok(true, "Manifest page displays a 'no manifest' message");
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_manifest-open-json.js b/devtools/client/application/test/browser/browser_application_panel_manifest-open-json.js
new file mode 100644
index 0000000000..7869502d4b
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_manifest-open-json.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the application panel fetches a manifest when in the Manifest Page
+ */
+
+add_task(async function () {
+ info("Test that manifest page has a link that opens the manifest JSON file");
+ const url = URL_ROOT_SSL + "resources/manifest/load-ok-manifest-link.html";
+ const manifestUrl = URL_ROOT_SSL + "resources/manifest/manifest.json";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest JSON link");
+ await waitUntil(() => doc.querySelector(".js-manifest-json-link") !== null);
+ ok(true, "Link to JSON is displayed");
+
+ info("Click on link and wait till the JSON is opened in a new tab");
+ // click on the link and wait for the new tab to open
+ const onTabLoaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ `${manifestUrl}`
+ );
+ const link = doc.querySelector(".js-manifest-json-link");
+ link.click();
+ const jsonTab = await onTabLoaded;
+ ok(jsonTab, "The manifest JSON was opened in a new tab");
+
+ // close the tabs
+ info("Closing the page tab.");
+ await BrowserTestUtils.removeTab(tab);
+ info("Closing the manifest JSON tab.");
+ await BrowserTestUtils.removeTab(jsonTab);
+});
+
+add_task(async function () {
+ info(
+ "Test that manifest page does not show a link for manifests embedded in a data url"
+ );
+ const url = URL_ROOT_SSL + "resources/manifest/load-ok.html";
+
+ await enableApplicationPanel();
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest to load");
+ await waitUntil(() => doc.querySelector(".js-manifest") !== null);
+ ok(true, "Manifest loaded successfully");
+ is(
+ doc.querySelector(".js-manifest-json-link"),
+ null,
+ "No JSON link is shown"
+ );
+
+ // close tab
+ info("Closing the tab");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_manifest-reload.js b/devtools/client/application/test/browser/browser_application_panel_manifest-reload.js
new file mode 100644
index 0000000000..4b8ab50985
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_manifest-reload.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the application panel refetches the page manifest when reloading
+ * or navigating to a new page
+ */
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ info("Loading a page with no manifest");
+ let url = URL_ROOT_SSL + "resources/manifest/load-no-manifest.html";
+ const { panel, tab } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the 'no manifest' message to appear");
+ await waitFor(() => doc.querySelector(".js-manifest-empty") !== null);
+ ok(true, "Manifest page displays a 'no manifest' message");
+
+ info("Navigating to a page with a manifest");
+ url = URL_ROOT_SSL + "resources/manifest/load-ok.html";
+ await navigateTo(url);
+
+ info("Waiting for the manifest to show up");
+ await waitFor(() => doc.querySelector(".js-manifest") !== null);
+ ok(true, "Manifest displayed successfully");
+
+ info("Navigating to a page with a manifest that fails to load");
+ url = URL_ROOT_SSL + "resources/manifest/load-fail.html";
+ await navigateTo(url);
+
+ info("Waiting for the manifest to fail to load");
+ await waitFor(() => doc.querySelector(".js-manifest-loaded-error") !== null);
+ ok(true, "Manifest page displays loading error");
+
+ info("Reloading");
+ await navigateTo(url);
+
+ info("Waiting for the manifest to fail to load");
+ await waitFor(() => doc.querySelector(".js-manifest-loaded-error") !== null);
+ ok(true, "Manifest page displays loading error");
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_open-links.js b/devtools/client/application/test/browser/browser_application_panel_open-links.js
new file mode 100644
index 0000000000..d654873827
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_open-links.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Toolbox } = require("resource://devtools/client/framework/toolbox.js");
+
+/**
+ * Check that links work when the devtools are detached in a separate window.
+ */
+
+const TAB_URL = URL_ROOT + "resources/service-workers/empty.html";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, toolbox } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ // detach devtools in a separate window
+ await toolbox.switchHost(Toolbox.HostType.WINDOW);
+
+ // click on the link and wait for the new tab to open
+ const onTabLoaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:debugging#workers",
+ true
+ );
+ doc.querySelector(".js-trusted-link").click();
+ info("Opening link in a new tab.");
+ const newTab = await onTabLoaded;
+
+ // We only need to check that newTab is truthy since
+ // BrowserTestUtils.waitForNewTab checks the URL.
+ ok(newTab, "The expected tab was opened.");
+
+ info("Wait until the main about debugging container is available");
+ await waitUntil(() => {
+ const aboutDebuggingDoc = newTab.linkedBrowser.contentDocument;
+ return aboutDebuggingDoc.querySelector(".app");
+ });
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(newTab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_sidebar.js b/devtools/client/application/test/browser/browser_application_panel_sidebar.js
new file mode 100644
index 0000000000..80b2fbfe09
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_sidebar.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the manifest is being properly shown
+ */
+
+add_task(async function () {
+ info("Test that we are displaying correctly the sidebar");
+
+ await enableApplicationPanel();
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel();
+ const doc = panel.panelWin.document;
+
+ info("Waiting for the sidebar to be displayed");
+ await waitUntil(() => doc.querySelector(".js-sidebar") !== null);
+ ok(true, "Sidebar is being displayed");
+
+ await waitUntil(() => doc.querySelector(".js-service-workers-page") !== null);
+ ok(true, "Service Workers page was loaded per default.");
+
+ // close the tab
+ info("Closing the tab.");
+ await commands.client.waitForRequestsToSettle();
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Test that we are displaying correctly the selected page - manifest");
+
+ await enableApplicationPanel();
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel();
+ const doc = panel.panelWin.document;
+
+ info("Select service worker page");
+ selectPage(panel, "service-workers");
+ await waitUntil(() => doc.querySelector(".js-service-workers-page") !== null);
+ await unregisterAllWorkers(commands.client, doc);
+
+ info("Select manifest page in the sidebar");
+ const link = doc.querySelector(".js-sidebar-manifest");
+ link.click();
+
+ await waitUntil(() => doc.querySelector(".js-manifest-page") !== null);
+ ok(true, "Manifest page was selected.");
+
+ // close the tab
+ info("Closing the tab.");
+ await commands.client.waitForRequestsToSettle();
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info(
+ "Test that we are displaying correctly the selected page - service workers"
+ );
+ const url = URL_ROOT + "resources/manifest/load-ok.html";
+
+ await enableApplicationPanel();
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel(url);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "manifest");
+
+ info("Waiting for the manifest to load");
+ await waitUntil(() => doc.querySelector(".js-manifest-page") !== null);
+ ok(true, "Manifest page was selected.");
+
+ info("Select service worker page in the sidebar");
+ const link = doc.querySelector(".js-sidebar-service-workers");
+ link.click();
+
+ await waitUntil(() => doc.querySelector(".js-service-workers-page") !== null);
+ ok(true, "Service workers page was selected.");
+
+ // close the tab
+ info("Closing the tab.");
+ await commands.client.waitForRequestsToSettle();
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_start-service-worker.js b/devtools/client/application/test/browser/browser_application_panel_start-service-worker.js
new file mode 100644
index 0000000000..1070f0f5af
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_start-service-worker.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = URL_ROOT + "resources/service-workers/simple.html";
+
+/**
+ * Tests that the Start button works for service workers who can be debugged
+ */
+add_task(async function () {
+ await enableApplicationPanel(); // this also enables SW debugging
+
+ // Setting a low idle_timeout and idle_extended_timeout will allow the service worker
+ // to reach the STOPPED state quickly, which will allow us to test the start button.
+ // The default value is 30000 milliseconds.
+ info("Set a low service worker idle timeout");
+ await pushPref("dom.serviceWorkers.idle_timeout", 1000);
+ await pushPref("dom.serviceWorkers.idle_extended_timeout", 1000);
+
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ await waitForWorkerRegistration(tab);
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ info("Wait until the start button is displayed and enabled");
+ const container = getWorkerContainers(doc)[0];
+ await waitUntil(() => {
+ const button = container.querySelector(".js-start-button");
+ return button && !button.disabled;
+ });
+
+ info("Click the button and wait for the worker to start");
+ const button = container.querySelector(".js-start-button");
+ button.click();
+
+ info("Wait until status 'Running' is displayed");
+ await waitUntil(() => {
+ const statusEl = container.querySelector(".js-worker-status");
+ return statusEl && statusEl.textContent === "Running";
+ });
+ ok(true, "Worker status is 'Running'");
+
+ await unregisterAllWorkers(commands.client, doc);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_target-switching.js b/devtools/client/application/test/browser/browser_application_panel_target-switching.js
new file mode 100644
index 0000000000..ff8d521e30
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_target-switching.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test switching for the top-level target.
+
+// We use about:robots, because this page will run in the parent process.
+// Navigating from about:robots to a regular content page will always trigger
+// a target switch, with or without fission.
+const PARENT_PROCESS_URI = "about:robots";
+const CONTENT_PROCESS_URI_WORKERS =
+ URL_ROOT + "resources/service-workers/simple.html";
+const CONTENT_PROCESS_URI_MANIFEST =
+ URL_ROOT + "resources/manifest/load-ok.html";
+
+// test workers when target switching
+add_task(async function () {
+ await enableApplicationPanel();
+
+ info("Open a page that runs in the parent process");
+ const { panel, commands, tab } = await openNewTabAndApplicationPanel(
+ PARENT_PROCESS_URI
+ );
+ const doc = panel.panelWin.document;
+
+ info("Check for non-existing service worker");
+ selectPage(panel, "service-workers");
+ const isWorkerListEmpty = !!doc.querySelector(".js-registration-list-empty");
+ ok(isWorkerListEmpty, "No Service Worker displayed");
+
+ info("Navigate to a page that runs in the child process");
+ await navigateTo(CONTENT_PROCESS_URI_WORKERS);
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ // close the tab
+ info("Closing the tab.");
+ await unregisterAllWorkers(commands.client, doc);
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// test manifest when target switching
+add_task(async function () {
+ await enableApplicationPanel();
+
+ info("Open a page that runs in the parent process");
+ const { panel, tab } = await openNewTabAndApplicationPanel(
+ PARENT_PROCESS_URI
+ );
+ const doc = panel.panelWin.document;
+
+ info("Waiting for the 'no manifest' message to appear");
+ selectPage(panel, "manifest");
+ await waitUntil(() => doc.querySelector(".js-manifest-empty") !== null);
+
+ info("Navigate to a page that runs in the child process");
+ await navigateTo(CONTENT_PROCESS_URI_MANIFEST);
+
+ info("Waiting for the manifest to load");
+ selectPage(panel, "manifest");
+ await waitUntil(() => doc.querySelector(".js-manifest") !== null);
+ ok(true, "Manifest loaded successfully");
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_telemetry-debug-worker.js b/devtools/client/application/test/browser/browser_application_panel_telemetry-debug-worker.js
new file mode 100644
index 0000000000..fe95dabd5e
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_telemetry-debug-worker.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = URL_ROOT + "resources/service-workers/simple.html";
+
+// check telemetry for debugging a service worker
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, tab, toolbox, commands } = await openNewTabAndApplicationPanel(
+ TAB_URL
+ );
+
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+ setupTelemetryTest();
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ const container = getWorkerContainers(doc)[0];
+ info("Wait until the debug link is displayed");
+ await waitUntil(() => {
+ return container.querySelector(".js-inspect-link");
+ });
+
+ info("Click on the debug link and wait for debugger to be ready");
+ const debugLink = container.querySelector(".js-inspect-link");
+ debugLink.click();
+ await waitUntil(() => toolbox.getPanel("jsdebugger"));
+
+ const events = getTelemetryEvents("jsdebugger");
+ const openToolboxEvent = events.find(event => event.method == "enter");
+ ok(openToolboxEvent.session_id > 0, "Event has a valid session id");
+ is(
+ openToolboxEvent.start_state,
+ "application",
+ "Event has the 'application' start state"
+ );
+
+ // clean up and close the tab
+ await unregisterAllWorkers(commands.client, doc);
+ info("Closing the tab.");
+ await commands.client.waitForRequestsToSettle();
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_telemetry-select-page.js b/devtools/client/application/test/browser/browser_application_panel_telemetry-select-page.js
new file mode 100644
index 0000000000..c200c38c17
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_telemetry-select-page.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const TAB_URL = URL_ROOT + "resources/service-workers/empty.html";
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ setupTelemetryTest();
+
+ // make sure the default page is opened and then select a different one
+ await waitUntil(() => doc.querySelector(".js-service-workers-page") !== null);
+ ok(true, "Service Workers page was loaded per default.");
+ selectPage(panel, "manifest");
+
+ checkTelemetryEvent({ method: "select_page", page_type: "manifest" });
+
+ // close the tab
+ info("Closing the tab.");
+ await commands.client.waitForRequestsToSettle();
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_telemetry-start-worker.js b/devtools/client/application/test/browser/browser_application_panel_telemetry-start-worker.js
new file mode 100644
index 0000000000..235ce3060d
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_telemetry-start-worker.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = URL_ROOT + "resources/service-workers/simple.html";
+
+// check telemetry for starting a service worker
+add_task(async function () {
+ info("Set a low service worker idle timeout");
+ await pushPref("dom.serviceWorkers.idle_timeout", 1000);
+ await pushPref("dom.serviceWorkers.idle_extended_timeout", 1000);
+
+ await enableApplicationPanel();
+
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+ await waitForWorkerRegistration(tab);
+
+ setupTelemetryTest();
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ info("Wait until the start button is displayed and enabled");
+ const container = getWorkerContainers(doc)[0];
+ await waitUntil(() => {
+ const button = container.querySelector(".js-start-button");
+ return button && !button.disabled;
+ });
+
+ info("Click the start button");
+ const button = container.querySelector(".js-start-button");
+ button.click();
+
+ checkTelemetryEvent({ method: "start_worker" });
+
+ // clean up and close the tab
+ await unregisterAllWorkers(commands.client, doc);
+ info("Closing the tab.");
+ await commands.client.waitForRequestsToSettle();
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_telemetry-unregister-worker.js b/devtools/client/application/test/browser/browser_application_panel_telemetry-unregister-worker.js
new file mode 100644
index 0000000000..aa6d35f946
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_telemetry-unregister-worker.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = URL_ROOT + "resources/service-workers/simple.html";
+
+// check telemetry for unregistering a service worker
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ setupTelemetryTest();
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ const workerContainer = getWorkerContainers(doc)[0];
+
+ info("Wait until the unregister button is displayed for the service worker");
+ await waitUntil(() => workerContainer.querySelector(".js-unregister-button"));
+ info("Click the unregister button");
+ const button = workerContainer.querySelector(".js-unregister-button");
+ button.click();
+
+ checkTelemetryEvent({ method: "unregister_worker" });
+
+ // clean up and close the tab
+ await unregisterAllWorkers(commands.client, doc);
+ info("Closing the tab.");
+ await commands.client.waitForRequestsToSettle();
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_unregister-worker.js b/devtools/client/application/test/browser/browser_application_panel_unregister-worker.js
new file mode 100644
index 0000000000..8f6c4a5793
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_unregister-worker.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = URL_ROOT + "resources/service-workers/simple.html";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ const workerContainer = getWorkerContainers(doc)[0];
+
+ info("Wait until the unregister button is displayed for the service worker");
+ await waitUntil(() => workerContainer.querySelector(".js-unregister-button"));
+ info("Click the unregister button");
+ const button = workerContainer.querySelector(".js-unregister-button");
+ button.click();
+ info("Wait until the service worker is removed from the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 0);
+ ok(true, "Service worker list is empty");
+
+ // just in case cleanup
+ await unregisterAllWorkers(commands.client, doc);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_viewsource-service-worker.js b/devtools/client/application/test/browser/browser_application_panel_viewsource-service-worker.js
new file mode 100644
index 0000000000..fc6f0819b6
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_viewsource-service-worker.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = URL_ROOT + "resources/service-workers/debug.html";
+const SW_URL = URL_ROOT + "resources/service-workers/debug-sw.js";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ // disable service worker debugging
+ await pushPref(
+ "devtools.debugger.features.windowless-service-workers",
+ false
+ );
+
+ const { panel, tab, commands } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+ const container = getWorkerContainers(doc)[0];
+ info("Wait until the inspect link is displayed");
+ await waitUntil(() => {
+ return container.querySelector(".js-inspect-link");
+ });
+
+ info("Click on the inspect link and wait for a new view-source: tab open");
+ // click on the link and wait for the new tab to open
+ const onTabLoaded = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ `view-source:${SW_URL}`
+ );
+ const inspectLink = container.querySelector(".js-inspect-link");
+ inspectLink.click();
+
+ const sourceTab = await onTabLoaded;
+ ok(sourceTab, "The service worker source was opened in a new tab");
+
+ // clean up
+ await unregisterAllWorkers(commands.client, doc);
+ // close the tabs
+ info("Closing the tabs.");
+ await BrowserTestUtils.removeTab(sourceTab);
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/browser_application_panel_worker-states.js b/devtools/client/application/test/browser/browser_application_panel_worker-states.js
new file mode 100644
index 0000000000..54446506fa
--- /dev/null
+++ b/devtools/client/application/test/browser/browser_application_panel_worker-states.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = URL_ROOT + "resources/service-workers/controlled-install.html";
+
+add_task(async function () {
+ await enableApplicationPanel();
+
+ const { panel, tab } = await openNewTabAndApplicationPanel(TAB_URL);
+ const doc = panel.panelWin.document;
+
+ selectPage(panel, "service-workers");
+
+ info("Check for non-existing service worker");
+ const isWorkerListEmpty = !!doc.querySelector(".js-registration-list-empty");
+ ok(isWorkerListEmpty, "No Service Worker displayed");
+
+ info("Register a service worker with a controlled install in the page.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.wrappedJSObject.registerServiceWorker();
+ });
+
+ info("Wait until the service worker appears in the application panel");
+ await waitUntil(() => !!getWorkerContainers(doc).length);
+ info("Wait until the 'Installing' state is displayed");
+ await waitUntil(() => {
+ const containers = getWorkerContainers(doc);
+ if (containers.length === 0) {
+ return false;
+ }
+
+ const stateEl = containers[0].querySelector(".js-worker-status");
+ return stateEl.textContent.toLowerCase() === "installing";
+ });
+
+ info("Allow the service worker to complete installation");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.wrappedJSObject.installServiceWorker();
+ });
+
+ info("Wait until the 'running' state is displayed");
+ await waitUntil(() => {
+ const workerContainer = getWorkerContainers(doc)[0];
+ const stateEl = workerContainer.querySelector(".js-worker-status");
+ return stateEl.textContent.toLowerCase() === "running";
+ });
+
+ info("Unregister the service worker");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const registration = await content.wrappedJSObject.sw;
+ registration.unregister();
+ });
+
+ info("Wait until the service worker is removed from the application panel");
+ await waitUntil(() => getWorkerContainers(doc).length === 0);
+
+ // close the tab
+ info("Closing the tab.");
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/devtools/client/application/test/browser/head.js b/devtools/client/application/test/browser/head.js
new file mode 100644
index 0000000000..6b7e67ff8d
--- /dev/null
+++ b/devtools/client/application/test/browser/head.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+/**
+ * Set all preferences needed to enable service worker debugging and testing.
+ */
+async function enableServiceWorkerDebugging() {
+ // Enable service workers.
+ await pushPref("dom.serviceWorkers.enabled", true);
+ // Accept workers from mochitest's http.
+ await pushPref("dom.serviceWorkers.testing.enabled", true);
+ // Force single content process, see Bug 1231208 for the SW refactor that should enable
+ // SW debugging in multi-e10s.
+ await pushPref("dom.ipc.processCount", 1);
+
+ // Enable service workers in the debugger
+ await pushPref("devtools.debugger.features.windowless-service-workers", true);
+ // Disable randomly spawning processes during tests
+ await pushPref("dom.ipc.processPrelaunch.enabled", false);
+
+ // Wait for dom.ipc.processCount to be updated before releasing processes.
+ Services.ppmm.releaseCachedProcesses();
+}
+
+async function enableApplicationPanel() {
+ // FIXME bug 1575427 this rejection is very common.
+ const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+ );
+ PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /this._frontCreationListeners is null/
+ );
+
+ // Enable all preferences related to service worker debugging.
+ await enableServiceWorkerDebugging();
+
+ // Enable web manifest processing.
+ Services.prefs.setBoolPref("dom.manifest.enabled", true);
+
+ // Enable application panel in DevTools.
+ await pushPref("devtools.application.enabled", true);
+}
+
+function setupTelemetryTest() {
+ // Reset all the counts
+ Services.telemetry.clearEvents();
+
+ // Ensure no events have been logged
+ const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+ const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
+ ok(!snapshot.parent, "No events have been logged for the main process");
+}
+
+function getTelemetryEvents(objectName) {
+ // read the requested events only
+ const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+ const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true);
+ // filter and transform the event data so the relevant info is in a single object:
+ // { method: "...", extraField: "...", anotherExtraField: "...", ... }
+ const events = snapshot.parent
+ .filter(event => event[1] === "devtools.main" && event[3] === objectName)
+ .map(event => ({ method: event[2], ...event[5] }));
+
+ return events;
+}
+
+function checkTelemetryEvent(expectedEvent, objectName = "application") {
+ info("Check telemetry event");
+ const events = getTelemetryEvents(objectName);
+
+ // assert we only got 1 event with a valid session ID
+ is(events.length, 1, "There was only 1 event logged");
+ const [event] = events;
+ ok(event.session_id > 0, "There is a valid session_id in the event");
+
+ // assert expected data
+ Assert.deepEqual(event, { ...expectedEvent, session_id: event.session_id });
+}
+
+function getWorkerContainers(doc) {
+ return doc.querySelectorAll(".js-sw-container");
+}
+
+async function openNewTabAndApplicationPanel(url) {
+ const tab = await addTab(url);
+
+ const toolbox = await gDevTools.showToolboxForTab(tab, {
+ toolId: "application",
+ });
+ const panel = toolbox.getCurrentPanel();
+ const target = toolbox.target;
+ const commands = toolbox.commands;
+ return { panel, tab, target, toolbox, commands };
+}
+
+async function unregisterAllWorkers(client, doc) {
+ // This method is declared in shared-head.js
+ await unregisterAllServiceWorkers(client);
+
+ info("Wait for service workers to disappear from the UI");
+ waitUntil(() => getWorkerContainers(doc).length === 0);
+}
+
+async function waitForWorkerRegistration(swTab) {
+ info("Wait until the registration appears on the window");
+ const swBrowser = swTab.linkedBrowser;
+ await asyncWaitUntil(async () =>
+ SpecialPowers.spawn(swBrowser, [], function () {
+ return !!content.wrappedJSObject.getRegistration();
+ })
+ );
+}
+
+function selectPage(panel, page) {
+ /**
+ * Select a page by simulating a user click in the sidebar.
+ * @param {string} page The page we want to select (see `PAGE_TYPES`)
+ **/
+ info(`Selecting application page: ${page}`);
+ const doc = panel.panelWin.document;
+ const navItem = doc.querySelector(`.js-sidebar-${page}`);
+ navItem.click();
+}
diff --git a/devtools/client/application/test/browser/resources/manifest/icon.svg b/devtools/client/application/test/browser/resources/manifest/icon.svg
new file mode 100644
index 0000000000..bfed2982bc
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/icon.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 953.37 984"><defs><linearGradient id="linear-gradient" x1="-14706.28" y1="9250.14" x2="-14443.04" y2="9250.14" gradientTransform="matrix(0.76, 0.03, 0.05, -1.12, 11485.47, 11148)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0083ff"/><stop offset="0.1" stop-color="#0092f8"/><stop offset="0.31" stop-color="#00abeb"/><stop offset="0.52" stop-color="#00bee1"/><stop offset="0.75" stop-color="#00c8dc"/><stop offset="1" stop-color="#00ccda"/></linearGradient><radialGradient id="radial-gradient" cx="-7588.66" cy="8866.53" r="791.23" gradientTransform="matrix(1.23, 0, 0, -1.22, 9958.21, 11048.11)" gradientUnits="userSpaceOnUse"><stop offset="0.02" stop-color="#005fe7"/><stop offset="0.18" stop-color="#0042b4"/><stop offset="0.32" stop-color="#002989"/><stop offset="0.4" stop-color="#002079"/><stop offset="0.47" stop-color="#131d78"/><stop offset="0.66" stop-color="#3b1676"/><stop offset="0.75" stop-color="#4a1475"/></radialGradient><linearGradient id="linear-gradient-2" x1="539.64" y1="254.8" x2="348.2" y2="881.03" gradientTransform="matrix(1, 0, 0, -1, 1, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000f43" stop-opacity="0.4"/><stop offset="0.48" stop-color="#001962" stop-opacity="0.17"/><stop offset="1" stop-color="#002079" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-3" x1="540.64" y1="254.8" x2="349.2" y2="881.03" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" href="#linear-gradient-2"/><linearGradient id="linear-gradient-4" x1="-8367.12" y1="7348.87" x2="-8482.36" y2="7357.76" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#812cc9"/><stop offset="1" stop-color="#005fe7"/></linearGradient><linearGradient id="linear-gradient-5" x1="-8449.89" y1="7496.97" x2="-8341.94" y2="7609.09" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0.05" stop-color="#005fe7"/><stop offset="0.18" stop-color="#065de6"/><stop offset="0.35" stop-color="#1856e1"/><stop offset="0.56" stop-color="#354adb"/><stop offset="0.78" stop-color="#5d3ad1"/><stop offset="0.95" stop-color="#812cc9"/></linearGradient><linearGradient id="linear-gradient-6" x1="-8653.41" y1="7245.3" x2="-8422.52" y2="7244.76" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#002079"/><stop offset="0.99" stop-color="#a238ff"/></linearGradient><radialGradient id="radial-gradient-2" cx="644.11" cy="599.83" fx="785.0454815336918" fy="470.6889181532662" r="793.95" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0.2" stop-color="#00fdff"/><stop offset="0.26" stop-color="#0af1ff"/><stop offset="0.37" stop-color="#23d2ff"/><stop offset="0.52" stop-color="#4da0ff"/><stop offset="0.69" stop-color="#855bff"/><stop offset="0.77" stop-color="#a238ff"/><stop offset="0.81" stop-color="#a738fd"/><stop offset="0.86" stop-color="#b539f9"/><stop offset="0.9" stop-color="#cd39f1"/><stop offset="0.96" stop-color="#ee3ae6"/><stop offset="0.98" stop-color="#ff3be0"/></radialGradient><linearGradient id="linear-gradient-7" x1="-7458.97" y1="9093.17" x2="-7531.06" y2="8282.84" gradientTransform="matrix(1.23, 0, 0, -1.22, 9958.21, 11048.11)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00ec00"/><stop offset="0.1" stop-color="#00e244"/><stop offset="0.22" stop-color="#00d694"/><stop offset="0.31" stop-color="#00cfc7"/><stop offset="0.35" stop-color="#00ccda"/><stop offset="0.42" stop-color="#0bc2dd" stop-opacity="0.92"/><stop offset="0.57" stop-color="#29a7e4" stop-opacity="0.72"/><stop offset="0.77" stop-color="#597df0" stop-opacity="0.4"/><stop offset="1" stop-color="#9448ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-8" x1="-8926.61" y1="7680.53" x2="-8790.14" y2="7680.53" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#005fe7"/><stop offset="0.46" stop-color="#0071f3" stop-opacity="0.51"/><stop offset="0.83" stop-color="#007efc" stop-opacity="0.14"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient><radialGradient id="radial-gradient-3" cx="-8914.62" cy="7721.05" r="165.97" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0.63" stop-color="#ffe302" stop-opacity="0"/><stop offset="0.67" stop-color="#ffe302" stop-opacity="0.05"/><stop offset="0.75" stop-color="#ffe302" stop-opacity="0.19"/><stop offset="0.86" stop-color="#ffe302" stop-opacity="0.4"/><stop offset="0.99" stop-color="#ffe302" stop-opacity="0.7"/></radialGradient><linearGradient id="linear-gradient-9" x1="214.02" y1="2032.47" x2="96.19" y2="2284.31" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, -250.1, 2306.29)" gradientUnits="userSpaceOnUse"><stop offset="0.19" stop-color="#4a1475" stop-opacity="0.5"/><stop offset="0.62" stop-color="#2277ac" stop-opacity="0.23"/><stop offset="0.94" stop-color="#00ccda" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-10" x1="-38.44" y1="278.18" x2="55.67" y2="171.29" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, 229.04, 745.87)" gradientUnits="userSpaceOnUse"><stop offset="0.01" stop-color="#002079" stop-opacity="0.5"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-11" x1="142.45" y1="96.25" x2="142.5" y2="149.68" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, 229.04, 745.87)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4a1475" stop-opacity="0.9"/><stop offset="0.18" stop-color="#6720a2" stop-opacity="0.6"/><stop offset="0.38" stop-color="#812acb" stop-opacity="0.34"/><stop offset="0.57" stop-color="#9332e8" stop-opacity="0.15"/><stop offset="0.76" stop-color="#9e36f9" stop-opacity="0.04"/><stop offset="0.93" stop-color="#a238ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-12" x1="620.52" y1="947.88" x2="926.18" y2="264.39" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00ec00" stop-opacity="0"/><stop offset="0.28" stop-color="#00dc6d" stop-opacity="0.5"/><stop offset="0.5" stop-color="#00d1bb" stop-opacity="0.86"/><stop offset="0.6" stop-color="#00ccda"/><stop offset="0.68" stop-color="#04c9db"/><stop offset="0.75" stop-color="#0fc1df"/><stop offset="0.83" stop-color="#23b2e6"/><stop offset="0.9" stop-color="#3e9ef0"/><stop offset="0.98" stop-color="#6184fc"/><stop offset="0.99" stop-color="#6680fe"/></linearGradient><linearGradient id="linear-gradient-13" x1="680.88" y1="554.79" x2="536.1" y2="166.04" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0083ff"/><stop offset="0.04" stop-color="#0083ff" stop-opacity="0.92"/><stop offset="0.14" stop-color="#0083ff" stop-opacity="0.71"/><stop offset="0.26" stop-color="#0083ff" stop-opacity="0.52"/><stop offset="0.37" stop-color="#0083ff" stop-opacity="0.36"/><stop offset="0.49" stop-color="#0083ff" stop-opacity="0.23"/><stop offset="0.61" stop-color="#0083ff" stop-opacity="0.13"/><stop offset="0.73" stop-color="#0083ff" stop-opacity="0.06"/><stop offset="0.86" stop-color="#0083ff" stop-opacity="0.01"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient></defs><title>firefox-logo-nightly</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="Layer_2-2" data-name="Layer 2"><g id="Firefox"><path d="M770.28,91.56c-23.95,27.88-35.1,90.64-10.82,154.26s61.5,49.8,84.7,114.67c30.62,85.6,16.37,200.59,16.37,200.59s36.81,106.61,62.47-6.63C979.79,341.74,770.28,143.94,770.28,91.56Z" style="fill:url(#linear-gradient)"/><path id="_Path_" data-name=" Path " d="M476.92,972.83c245.24,0,443.9-199.74,443.9-446s-198.66-446-443.66-446S33.5,280.51,33.5,526.8C33,773.33,231.92,972.83,476.92,972.83Z" style="fill:url(#radial-gradient)"/><path d="M810.67,803.64a246.8,246.8,0,0,1-30.12,18.18,705.31,705.31,0,0,0,38.3-63c9.46-10.47,18.13-20.65,25.19-31.65,3.44-5.41,7.31-12.08,11.42-19.82,24.92-44.9,52.4-117.56,53.18-192.2v-5.66a257.25,257.25,0,0,0-5.71-55.75c.2,1.43.38,2.86.56,4.29-.22-1.1-.41-2.21-.64-3.31.37,2,.66,4,1,6,5.09,43.22,1.47,85.37-16.68,116.45-.29.45-.58.88-.87,1.32,9.41-47.23,12.56-99.39,2.09-151.6,0,0-4.19-25.38-35.38-102.44-18-44.35-49.83-80.72-78-107.21-24.69-30.55-47.11-51-59.47-64.06C689.72,126,678.9,105.61,674.45,92.31c-3.85-1.93-53.14-49.81-57.05-51.63-21.51,33.35-89.16,137.67-57,235.15,14.58,44.17,51.47,90,90.07,115.74,1.69,1.94,23,25,33.09,77.16,10.45,53.85,5,95.86-16.54,158C641.73,681.24,577,735.12,516.3,740.63c-129.67,11.78-177.15-65.11-177.15-65.11C385.49,694,436.72,690.17,467.87,671c31.4-19.43,50.39-33.83,65.81-28.15C548.86,648.43,561,632,550.1,615a78.5,78.5,0,0,0-79.4-34.57c-31.43,5.11-60.23,30-101.41,5.89a86.29,86.29,0,0,1-7.73-5.06c-2.71-1.79,8.83,2.72,6.13.69-8-4.35-22.2-13.84-25.88-17.22-.61-.56,6.22,2.18,5.61,1.62-38.51-31.71-33.7-53.13-32.49-66.57,1-10.75,8-24.52,19.75-30.11,5.69,3.11,9.24,5.48,9.24,5.48s-2.43-5-3.74-7.58c.46-.2.9-.15,1.36-.34,4.66,2.25,15,8.1,20.41,11.67,7.07,5,9.33,9.44,9.33,9.44s1.86-1,.48-5.37c-.5-1.78-2.65-7.45-9.65-13.17h.44A81.61,81.61,0,0,1,374.42,478c2-7.18,5.53-14.68,4.75-28.09-.48-9.43-.26-11.87-1.92-15.51-1.49-3.13.83-4.35,3.42-1.1a32.5,32.5,0,0,0-2.21-7.4v-.24c3.23-11.24,68.25-40.46,73-43.88A67.2,67.2,0,0,0,470.59,361c3.62-5.76,6.34-13.85,7-26.11.36-8.84-3.76-14.73-69.51-21.62-18-1.77-28.53-14.8-34.53-26.82-1.09-2.59-2.21-4.94-3.33-7.28a57.68,57.68,0,0,1-2.56-8.43c10.75-30.87,28.81-57,55.37-76.7,1.45-1.32-5.78.34-4.34-1,1.69-1.54,12.71-6,14.79-7,2.54-1.2-10.88-6.9-22.73-5.51-12.07,1.36-14.63,2.8-21.07,5.53,2.67-2.66,11.17-6.15,9.18-6.13-13,2-29.18,9.56-43,18.12a10.66,10.66,0,0,1,.83-4.35c-6.44,2.73-22.26,13.79-26.87,23.14a44.29,44.29,0,0,0,.27-5.4,84.17,84.17,0,0,0-13.19,13.82l-.24.22c-37.36-15-70.23-16-98.05-9.28-6.09-6.11-9.06-1.64-22.91-32.07-.94-1.83.72,1.81,0,0-2.28-5.9,1.39,7.87,0,0-23.28,18.37-53.92,39.19-68.63,53.89-.18.59,17.16-4.9,0,0-6,1.72-5.6,5.28-6.51,37.5-.22,2.44,0,5.18-.22,7.38-11.75,15-19.75,27.64-22.78,34.21-15.19,26.18-31.93,67-48.15,131.55A334.82,334.82,0,0,1,75.2,398.36C61.71,432.63,48.67,486.44,46.07,569.3A482.08,482.08,0,0,1,58.6,518.64,473,473,0,0,0,93.33,719.71c9.33,22.82,24.76,57.46,51,95.4C226.9,902,343.31,956,472.21,956,606.79,956,727.64,897.13,810.67,803.64Z" style="fill:url(#linear-gradient-2)"/><path d="M810.67,803.64a246.8,246.8,0,0,1-30.12,18.18,705.31,705.31,0,0,0,38.3-63c9.46-10.47,18.13-20.65,25.19-31.65,3.44-5.41,7.31-12.08,11.42-19.82,24.92-44.9,52.4-117.56,53.18-192.2v-5.66a257.25,257.25,0,0,0-5.71-55.75c.2,1.43.38,2.86.56,4.29-.22-1.1-.41-2.21-.64-3.31.37,2,.66,4,1,6,5.09,43.22,1.47,85.37-16.68,116.45-.29.45-.58.88-.87,1.32,9.41-47.23,12.56-99.39,2.09-151.6,0,0-4.19-25.38-35.38-102.44-18-44.35-49.83-80.72-78-107.21-24.69-30.55-47.11-51-59.47-64.06C689.72,126,678.9,105.61,674.45,92.31c-3.85-1.93-53.14-49.81-57.05-51.63-21.51,33.35-89.16,137.67-57,235.15,14.58,44.17,51.47,90,90.07,115.74,1.69,1.94,23,25,33.09,77.16,10.45,53.85,5,95.86-16.54,158C641.73,681.24,577,735.12,516.3,740.63c-129.67,11.78-177.15-65.11-177.15-65.11C385.49,694,436.72,690.17,467.87,671c31.4-19.43,50.39-33.83,65.81-28.15C548.86,648.43,561,632,550.1,615a78.5,78.5,0,0,0-79.4-34.57c-31.43,5.11-60.23,30-101.41,5.89a86.29,86.29,0,0,1-7.73-5.06c-2.71-1.79,8.83,2.72,6.13.69-8-4.35-22.2-13.84-25.88-17.22-.61-.56,6.22,2.18,5.61,1.62-38.51-31.71-33.7-53.13-32.49-66.57,1-10.75,8-24.52,19.75-30.11,5.69,3.11,9.24,5.48,9.24,5.48s-2.43-5-3.74-7.58c.46-.2.9-.15,1.36-.34,4.66,2.25,15,8.1,20.41,11.67,7.07,5,9.33,9.44,9.33,9.44s1.86-1,.48-5.37c-.5-1.78-2.65-7.45-9.65-13.17h.44A81.61,81.61,0,0,1,374.42,478c2-7.18,5.53-14.68,4.75-28.09-.48-9.43-.26-11.87-1.92-15.51-1.49-3.13.83-4.35,3.42-1.1a32.5,32.5,0,0,0-2.21-7.4v-.24c3.23-11.24,68.25-40.46,73-43.88A67.2,67.2,0,0,0,470.59,361c3.62-5.76,6.34-13.85,7-26.11.36-8.84-3.76-14.73-69.51-21.62-18-1.77-28.53-14.8-34.53-26.82-1.09-2.59-2.21-4.94-3.33-7.28a57.68,57.68,0,0,1-2.56-8.43c10.75-30.87,28.81-57,55.37-76.7,1.45-1.32-5.78.34-4.34-1,1.69-1.54,12.71-6,14.79-7,2.54-1.2-10.88-6.9-22.73-5.51-12.07,1.36-14.63,2.8-21.07,5.53,2.67-2.66,11.17-6.15,9.18-6.13-13,2-29.18,9.56-43,18.12a10.66,10.66,0,0,1,.83-4.35c-6.44,2.73-22.26,13.79-26.87,23.14a44.29,44.29,0,0,0,.27-5.4,84.17,84.17,0,0,0-13.19,13.82l-.24.22c-37.36-15-70.23-16-98.05-9.28-6.09-6.11-9.06-1.64-22.91-32.07-.94-1.83.72,1.81,0,0-2.28-5.9,1.39,7.87,0,0-23.28,18.37-53.92,39.19-68.63,53.89-.18.59,17.16-4.9,0,0-6,1.72-5.6,5.28-6.51,37.5-.22,2.44,0,5.18-.22,7.38-11.75,15-19.75,27.64-22.78,34.21-15.19,26.18-31.93,67-48.15,131.55A334.82,334.82,0,0,1,75.2,398.36C61.71,432.63,48.67,486.44,46.07,569.3A482.08,482.08,0,0,1,58.6,518.64,473,473,0,0,0,93.33,719.71c9.33,22.82,24.76,57.46,51,95.4C226.9,902,343.31,956,472.21,956,606.79,956,727.64,897.13,810.67,803.64Z" style="fill:url(#linear-gradient-3)"/><path d="M711.1,866.71c162.87-18.86,235-186.7,142.38-190C769.85,674,634,875.61,711.1,866.71Z" style="fill:url(#linear-gradient-4)"/><path d="M865.21,642.42C977.26,577.21,948,436.34,948,436.34s-43.25,50.24-72.62,130.32C846.4,646,797.84,681.81,865.21,642.42Z" style="fill:url(#linear-gradient-5)"/><path d="M509.47,950.06C665.7,999.91,800,876.84,717.21,835.74,642,798.68,435.32,926.49,509.47,950.06Z" style="fill:url(#linear-gradient-6)"/><path d="M638.58,21.42l.53-.57A1.7,1.7,0,0,0,638.58,21.42ZM876.85,702.23c3.8-5.36,8.94-22.53,13.48-30.21,27.58-44.52,27.78-80,27.78-80.84,16.66-83.22,15.15-117.2,4.9-180-8.25-50.6-44.32-123.09-75.57-158-32.2-36-9.51-24.25-40.69-50.52-27.33-30.29-53.82-60.29-68.25-72.36C634.22,43.09,636.57,24.58,638.58,21.42c-.34.37-.84.92-1.47,1.64C635.87,18.14,635,14,635,14s-57,57-69,152c-7.83,62,15.38,126.68,49,168a381.62,381.62,0,0,0,59,58h0c25.4,36.48,39.38,81.49,39.38,129.91,0,121.24-98.34,219.53-219.65,219.53a220.14,220.14,0,0,1-49.13-5.52c-57.24-10.92-90.3-39.8-106.78-59.41-9.45-11.23-13.46-19.42-13.46-19.42,51.28,18.37,108,14.53,142.47-4.52,34.75-19.26,55.77-33.55,72.84-27.92,16.82,5.61,30.21-10.67,18.2-27.54-11.77-16.85-42.4-41-87.88-34.29-34.79,5.07-66.66,29.76-112.24,5.84a97.34,97.34,0,0,1-8.55-5c-3-1.77,9.77,2.69,6.79.68-8.87-4.32-24.57-13.73-28.64-17.07-.68-.56,6.88,2.16,6.2,1.6-42.62-31.45-37.3-52.69-36-66,1.07-10.66,8.81-24.32,21.86-29.86,6.3,3.08,10.23,5.43,10.23,5.43s-2.69-4.92-4.14-7.51c.51-.19,1-.15,1.5-.34,5.16,2.23,16.58,8,22.59,11.57,7.83,4.95,10.32,9.36,10.32,9.36s2.06-1,.54-5.33c-.56-1.77-2.93-7.39-10.68-13.07h.48a91.65,91.65,0,0,1,13.13,8.17c2.19-7.12,6.12-14.56,5.25-27.86-.53-9.35-.28-11.78-2.12-15.39-1.65-3.1.92-4.31,3.78-1.09a29.73,29.73,0,0,0-2.44-7.34v-.24c3.57-11.14,75.53-40.12,80.77-43.51a70.24,70.24,0,0,0,21.17-20.63c4-5.72,7-13.73,7.75-25.89.25-5.48-1.44-9.82-20.5-14-11.44-2.49-29.14-4.91-56.43-7.47-19.9-1.76-31.58-14.68-38.21-26.6-1.21-2.57-2.45-4.9-3.68-7.22a53.41,53.41,0,0,1-2.83-8.36,158.47,158.47,0,0,1,61.28-76.06c1.6-1.31-6.4.33-4.8-1,1.87-1.52,14.06-5.93,16.37-6.92,2.81-1.19-12-6.84-25.16-5.47-13.36,1.35-16.19,2.78-23.32,5.49,3-2.64,12.37-6.1,10.16-6.08-14.4,2-32.3,9.48-47.6,18a9.72,9.72,0,0,1,.92-4.31c-7.13,2.71-24.64,13.67-29.73,23a39.79,39.79,0,0,0,.29-5.35,88.55,88.55,0,0,0-14.6,13.7l-.27.22C258.14,196,221.75,195,191,201.72c-6.74-6.06-17.57-15.23-32.89-45.4-1-1.82-1.6,3.75-2.4,2-6-13.81-9.55-36.44-9-52,0,0-12.32,5.61-22.51,29.06-1.89,4.21-3.11,6.54-4.32,8.87-.56.68,1.27-7.7,1-7.24-1.77,3-6.36,7.19-8.37,12.62-1.38,4-3.32,6.27-4.56,11.29l-.29.46c-.1-1.48.37-6.08,0-5.14A235.4,235.4,0,0,0,95.34,186c-5.49,18-11.88,42.61-12.89,74.57-.24,2.42,0,5.14-.25,7.32-13,14.83-21.86,27.39-25.2,33.91-16.81,26-35.33,66.44-53.29,130.46a319.35,319.35,0,0,1,28.54-50C17.32,416.25,2.89,469.62,0,551.8a436.92,436.92,0,0,1,13.87-50.24C11.29,556.36,17.68,624.3,52.32,701c20.57,45,67.92,136.6,183.62,208h0s39.36,29.3,107,51.26c5,1.81,10.06,3.6,15.23,5.33q-2.43-1-4.71-2A484.9,484.9,0,0,0,492.27,984c175.18.15,226.85-70.2,226.85-70.2l-.51.38q3.71-3.49,7.14-7.26c-27.64,26.08-90.75,27.84-114.3,26,40.22-11.81,66.69-21.81,118.17-41.52q9-3.36,18.48-7.64l2-.94c1.25-.58,2.49-1.13,3.75-1.74a349.3,349.3,0,0,0,70.26-44c51.7-41.3,63-81.56,68.83-108.1-.82,2.54-3.37,8.47-5.17,12.32-13.31,28.48-42.84,46-74.91,61a689.05,689.05,0,0,0,42.38-62.44C865.77,729.39,869,713.15,876.85,702.23Z" style="fill:url(#radial-gradient-2)"/><path d="M813.92,801c21.08-23.24,40-49.82,54.35-80,36.9-77.58,94-206.58,49-341.31C881.77,273.22,833,215,771.11,158.12,670.56,65.76,642.48,24.52,642.48,0c0,0-116.09,129.41-65.74,264.38s153.46,130,221.68,270.87c80.27,165.74-64.95,346.61-185,397.24,7.35-1.63,267-60.38,280.61-208.88C893.68,726.34,887.83,767.41,813.92,801Z" style="fill:url(#linear-gradient-7)"/><path d="M477.59,319.37c.39-8.77-4.16-14.66-76.68-21.46-29.84-2.76-41.26-30.33-44.75-41.94-10.61,27.56-15,56.49-12.64,91.48,1.61,22.92,17,47.52,24.37,62,0,0,1.64-2.13,2.39-2.91,13.86-14.43,71.94-36.42,77.39-39.54C453.69,363.16,476.58,346.44,477.59,319.37Z" style="fill:url(#linear-gradient-8)"/><path d="M477.59,319.37c.39-8.77-4.16-14.66-76.68-21.46-29.84-2.76-41.26-30.33-44.75-41.94-10.61,27.56-15,56.49-12.64,91.48,1.61,22.92,17,47.52,24.37,62,0,0,1.64-2.13,2.39-2.91,13.86-14.43,71.94-36.42,77.39-39.54C453.69,363.16,476.58,346.44,477.59,319.37Z" style="opacity:0.5;isolation:isolate;fill:url(#radial-gradient-3)"/><path d="M158.31,156.47c-1-1.82-1.6,3.75-2.4,2-6-13.81-9.58-36.2-8.72-52,0,0-12.32,5.61-22.51,29.06-1.89,4.21-3.11,6.54-4.32,8.86-.56.68,1.27-7.7,1-7.24-1.77,3-6.36,7.19-8.35,12.38-1.65,4.24-3.35,6.52-4.61,11.77-.39,1.43.39-6.32,0-5.38C84.72,201.68,80.19,271,82.69,268,133.17,214.14,191,201.36,191,201.36c-6.15-4.53-19.53-17.63-32.7-44.89Z" style="fill:url(#linear-gradient-9)"/><path d="M349.84,720.1c-69.72-29.77-149-71.75-146-167.14C207.92,427.35,321,452.18,321,452.18c-4.27,1-15.68,9.16-19.72,17.82-4.27,10.83-12.07,35.28,11.55,60.9,37.09,40.19-76.2,95.36,98.66,199.57,4.41,2.4-41-1.43-61.64-10.36Z" style="fill:url(#linear-gradient-10)"/><path d="M325.07,657.5c49.44,17.21,107,14.19,141.52-4.86,23.09-12.85,52.7-33.43,70.92-28.35-15.78-6.24-27.73-9.15-42.1-9.86-2.45,0-5.38,0-8-.32a136,136,0,0,0-15.76.86c-8.9.82-18.77,6.43-27.74,5.53-.48,0,8.7-3.77,8-3.61-4.75,1-9.92,1.21-15.37,1.88-3.47.39-6.45.82-9.89,1-103,8.73-190-55.81-190-55.81-7.41,25,33.17,74.3,88.52,93.57Z" style="opacity:0.5;isolation:isolate;fill:url(#linear-gradient-11)"/><path d="M813.74,801.65c104.16-102.27,156.86-226.58,134.58-366,0,0,8.9,71.5-24.85,144.63,16.21-71.39,18.1-160.11-25-252C841,205.64,746.45,141.11,710.35,114.19,655.66,73.4,633,31.87,632.57,23.3c-16.34,33.48-65.77,148.2-5.31,247,56.64,92.56,145.86,120,208.33,205C950.67,631.67,813.74,801.65,813.74,801.65Z" style="fill:url(#linear-gradient-12)"/><path d="M798.81,535.55C762.41,460.35,717,427.55,674,392c5,7,6.23,9.47,9,14,37.83,40.32,93.61,138.66,53.11,262.11C659.88,900.48,355,791.06,323,760.32,335.93,894.81,561,959.16,707.6,872,791,793,858.47,658.79,798.81,535.55Z" style="fill:url(#linear-gradient-13)"/></g></g></g></g></svg>
diff --git a/devtools/client/application/test/browser/resources/manifest/load-fail.html b/devtools/client/application/test/browser/resources/manifest/load-fail.html
new file mode 100644
index 0000000000..180c42a7b5
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/load-fail.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<head>
+ <meta charset="utf-8">
+ <title>Manifest 404 not found</title>
+ <link rel="manifest" href='nowhere.json'>
+</head>
+<body>
+<h1>Manifest error 404 not found</h1>
+</body>
diff --git a/devtools/client/application/test/browser/resources/manifest/load-no-manifest.html b/devtools/client/application/test/browser/resources/manifest/load-no-manifest.html
new file mode 100644
index 0000000000..aeabc8a0cb
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/load-no-manifest.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<head>
+ <meta charset="utf-8">
+ <title>No manifest link</title>
+</head>
+<body>
+<h1>No manifest <code>link</code> tag.</h1>
+</body>
diff --git a/devtools/client/application/test/browser/resources/manifest/load-ok-icons.html b/devtools/client/application/test/browser/resources/manifest/load-ok-icons.html
new file mode 100644
index 0000000000..539e5d2247
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/load-ok-icons.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<head>
+ <meta charset="utf-8">
+ <title>Manifest successful load (with icons)</title>
+ <link rel="manifest" href='data:application/manifest+json,{"name": "Foo", "background_color": "%23ff0000", "icons": [{ "sizes": "128x128", "src": "http://example.com/browser/devtools/client/application/test/browser/resources/manifest/icon.svg", "type": "image/svg"}]}'>
+</head>
+<body>
+<h1>Manifest OK (with icons)</h1>
+</body>
diff --git a/devtools/client/application/test/browser/resources/manifest/load-ok-json-error.html b/devtools/client/application/test/browser/resources/manifest/load-ok-json-error.html
new file mode 100644
index 0000000000..95ad22b609
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/load-ok-json-error.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<head>
+ <meta charset="utf-8">
+ <title>Manifest successful load with a warning</title>
+ <link rel="manifest" href='data:application/manifest+json,{"name": "Foo}'>
+</head>
+<body>
+<h1>Manifest OK with validation errors</h1>
+<p>The manifest has invalid JSON</p>
+</body>
diff --git a/devtools/client/application/test/browser/resources/manifest/load-ok-manifest-link.html b/devtools/client/application/test/browser/resources/manifest/load-ok-manifest-link.html
new file mode 100644
index 0000000000..f336f409e3
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/load-ok-manifest-link.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<head>
+ <meta charset="utf-8">
+ <title>Successful load for a linked manifest</title>
+ <link rel="manifest" href='manifest.json'>
+</head>
+<body>
+<h1>Manifest OK (linked manifest)</h1>
+</body>
diff --git a/devtools/client/application/test/browser/resources/manifest/load-ok-warnings.html b/devtools/client/application/test/browser/resources/manifest/load-ok-warnings.html
new file mode 100644
index 0000000000..467d6c3e70
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/load-ok-warnings.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<head>
+ <meta charset="utf-8">
+ <title>Manifest successful load with a warning</title>
+ <link rel="manifest" href='data:application/manifest+json,{"name": "Foo", "background_color": 42}'>
+</head>
+<body>
+<h1>Manifest OK with validation warnings</h1>
+<p><code>background_color</code> does not contain a valid CSS color.</p>
+</body>
diff --git a/devtools/client/application/test/browser/resources/manifest/load-ok.html b/devtools/client/application/test/browser/resources/manifest/load-ok.html
new file mode 100644
index 0000000000..1e6f5de59e
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/load-ok.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<head>
+ <meta charset="utf-8">
+ <title>Manifest successful load</title>
+ <link rel="manifest" href='data:application/manifest+json,{"name": "Foo", "background_color": "%23ff0000"}'>
+</head>
+<body>
+<h1>Manifest OK</h1>
+</body>
diff --git a/devtools/client/application/test/browser/resources/manifest/manifest.json b/devtools/client/application/test/browser/resources/manifest/manifest.json
new file mode 100644
index 0000000000..0bc7bb50a3
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/manifest/manifest.json
@@ -0,0 +1,3 @@
+{
+ "name": "Foo"
+}
diff --git a/devtools/client/application/test/browser/resources/service-workers/controlled-install-sw.js b/devtools/client/application/test/browser/resources/service-workers/controlled-install-sw.js
new file mode 100644
index 0000000000..d0a70a5312
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/controlled-install-sw.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Copied from shared-head.js
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function () {
+ waitUntil(predicate, interval).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+// this flag will be flipped externally from controlled-install.html
+// by sending a message event to the worker
+let canInstall = false;
+self.addEventListener("message", event => {
+ if (event.data === "install-service-worker") {
+ canInstall = true;
+ }
+});
+
+self.addEventListener("install", event => {
+ event.waitUntil(waitUntil(() => canInstall));
+});
diff --git a/devtools/client/application/test/browser/resources/service-workers/controlled-install.html b/devtools/client/application/test/browser/resources/service-workers/controlled-install.html
new file mode 100644
index 0000000000..300ee1fde7
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/controlled-install.html
@@ -0,0 +1,27 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+let registration;
+
+window.registerServiceWorker = async function() {
+ registration = await navigator.serviceWorker.register(
+ "controlled-install-sw.js"
+ );
+ window.sw = registration;
+};
+
+window.installServiceWorker = function() {
+ registration.installing.postMessage("install-service-worker");
+};
+</script>
+</body>
+</html>
diff --git a/devtools/client/application/test/browser/resources/service-workers/debug-sw.js b/devtools/client/application/test/browser/resources/service-workers/debug-sw.js
new file mode 100644
index 0000000000..31f0b1bdd2
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/debug-sw.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+self.addEventListener("activate", event => {
+ event.waitUntil(self.clients.claim());
+});
+
+self.onfetch = function (event) {
+ const url = event.request.url;
+
+ const response = url.endsWith("test")
+ ? new Response("lorem ipsum", { statusText: "OK" })
+ : fetch(event.request);
+
+ event.respondWith(response);
+};
diff --git a/devtools/client/application/test/browser/resources/service-workers/debug.html b/devtools/client/application/test/browser/resources/service-workers/debug.html
new file mode 100644
index 0000000000..f0f16858fd
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/debug.html
@@ -0,0 +1,25 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+window.sw = navigator.serviceWorker.register("debug-sw.js");
+
+/* exported fetchFromWorker */
+async function fetchFromWorker() {
+ const response = await fetch("test");
+ const text = await response.text();
+ console.log(`Response from worker: ${text}`);
+}
+</script>
+
+<p>This page has a <code>fetchFromWorker()</code> function.</p>
+</body>
+</html>
diff --git a/devtools/client/application/test/browser/resources/service-workers/dynamic-registration.html b/devtools/client/application/test/browser/resources/service-workers/dynamic-registration.html
new file mode 100644
index 0000000000..def300da65
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/dynamic-registration.html
@@ -0,0 +1,19 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+window.registerServiceWorker = function() {
+ window.sw = navigator.serviceWorker.register("empty-sw.js");
+};
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/application/test/browser/resources/service-workers/empty-sw.js b/devtools/client/application/test/browser/resources/service-workers/empty-sw.js
new file mode 100644
index 0000000000..5d33297056
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/empty-sw.js
@@ -0,0 +1,4 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Empty, just test registering.
diff --git a/devtools/client/application/test/browser/resources/service-workers/empty.html b/devtools/client/application/test/browser/resources/service-workers/empty.html
new file mode 100644
index 0000000000..02373ca02e
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/empty.html
@@ -0,0 +1,11 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test (no worker, empty page)</title>
+</head>
+<body></body>
+</html>
diff --git a/devtools/client/application/test/browser/resources/service-workers/scope-page.html b/devtools/client/application/test/browser/resources/service-workers/scope-page.html
new file mode 100644
index 0000000000..eed5bc82ed
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/scope-page.html
@@ -0,0 +1,19 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+window.sw = navigator.serviceWorker.register("empty-sw.js", {
+ scope: "./scope-page.html",
+});
+
+</script>
+</body>
+</html>
diff --git a/devtools/client/application/test/browser/resources/service-workers/simple-unicode.html b/devtools/client/application/test/browser/resources/service-workers/simple-unicode.html
new file mode 100644
index 0000000000..51e17b7fec
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/simple-unicode.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+window.sw = navigator.serviceWorker.register("empty-sw.js?q=日本");
+</script>
+</body>
+</html>
diff --git a/devtools/client/application/test/browser/resources/service-workers/simple.html b/devtools/client/application/test/browser/resources/service-workers/simple.html
new file mode 100644
index 0000000000..88dc00aff0
--- /dev/null
+++ b/devtools/client/application/test/browser/resources/service-workers/simple.html
@@ -0,0 +1,32 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+let registration;
+
+const registerServiceWorker = async function() {
+ try {
+ registration = await navigator.serviceWorker.register("empty-sw.js");
+ dump("Empty service worker registered\n");
+ } catch (e) {
+ dump("Empty service worker not registered: " + e + "\n");
+ }
+};
+
+// Helper called from head.js to unregister the service worker.
+window.getRegistration = function() {
+ return registration;
+};
+// Register the service worker.
+registerServiceWorker();
+</script>
+</body>
+</html>
diff --git a/devtools/client/application/test/node/.eslintrc.js b/devtools/client/application/test/node/.eslintrc.js
new file mode 100644
index 0000000000..ffb3e70473
--- /dev/null
+++ b/devtools/client/application/test/node/.eslintrc.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ jest: true,
+ },
+};
diff --git a/devtools/client/application/test/node/actions/actions_application_panel-manifest.test.js b/devtools/client/application/test/node/actions/actions_application_panel-manifest.test.js
new file mode 100644
index 0000000000..a8d4844f4c
--- /dev/null
+++ b/devtools/client/application/test/node/actions/actions_application_panel-manifest.test.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ MANIFEST_NO_ISSUES,
+} = require("resource://devtools/client/application/test/node/fixtures/data/constants.js");
+
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+
+const {
+ ManifestDevToolsError,
+ services,
+} = require("resource://devtools/client/application/src/modules/application-services.js");
+
+const {
+ FETCH_MANIFEST_FAILURE,
+ FETCH_MANIFEST_START,
+ FETCH_MANIFEST_SUCCESS,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const {
+ fetchManifest,
+} = require("resource://devtools/client/application/src/actions/manifest.js");
+
+describe("Manifest actions: fetchManifest", () => {
+ it("dispatches a START - SUCCESS sequence when fetching is OK", async () => {
+ const fetchManifestSpy = jest
+ .spyOn(services, "fetchManifest")
+ .mockResolvedValue(MANIFEST_NO_ISSUES);
+
+ const store = setupStore({});
+ await store.dispatch(fetchManifest());
+
+ expect(store.getActions()).toEqual([
+ { type: FETCH_MANIFEST_START },
+ { type: FETCH_MANIFEST_SUCCESS, manifest: MANIFEST_NO_ISSUES },
+ ]);
+
+ fetchManifestSpy.mockRestore();
+ });
+
+ it("dispatches a START - FAILURE sequence when fetching fails", async () => {
+ const fetchManifestSpy = jest
+ .spyOn(services, "fetchManifest")
+ .mockRejectedValue(new Error("lorem ipsum"));
+
+ const store = setupStore({});
+ await store.dispatch(fetchManifest());
+
+ expect(store.getActions()).toEqual([
+ { type: FETCH_MANIFEST_START },
+ { type: FETCH_MANIFEST_FAILURE, error: "lorem ipsum" },
+ ]);
+
+ fetchManifestSpy.mockRestore();
+ });
+
+ it("dispatches a START - FAILURE sequence when fetching fails due to a devtools error", async () => {
+ const error = new ManifestDevToolsError(":(");
+ const fetchManifestSpy = jest
+ .spyOn(services, "fetchManifest")
+ .mockRejectedValue(error);
+ const consoleErrorSpy = jest
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
+
+ const store = setupStore({});
+ await store.dispatch(fetchManifest());
+
+ expect(store.getActions()).toEqual([
+ { type: FETCH_MANIFEST_START },
+ { type: FETCH_MANIFEST_FAILURE, error: "manifest-loaded-devtools-error" },
+ ]);
+ expect(consoleErrorSpy).toHaveBeenCalledWith(error);
+
+ fetchManifestSpy.mockRestore();
+ consoleErrorSpy.mockRestore();
+ });
+});
diff --git a/devtools/client/application/test/node/babel.config.js b/devtools/client/application/test/node/babel.config.js
new file mode 100644
index 0000000000..a9e47ba7fb
--- /dev/null
+++ b/devtools/client/application/test/node/babel.config.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+module.exports = {
+ plugins: ["@babel/plugin-proposal-async-generator-functions"],
+};
diff --git a/devtools/client/application/test/node/components/__snapshots__/components_application_panel-App.test.js.snap b/devtools/client/application/test/node/components/__snapshots__/components_application_panel-App.test.js.snap
new file mode 100644
index 0000000000..cef0e0a85d
--- /dev/null
+++ b/devtools/client/application/test/node/components/__snapshots__/components_application_panel-App.test.js.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`App renders the expected snapshot 1`] = `
+<LocalizationProvider>
+ <main
+ className="app"
+ >
+ <Connect(Sidebar) />
+ <Connect(PageSwitcher) />
+ </main>
+</LocalizationProvider>
+`;
diff --git a/devtools/client/application/test/node/components/components_application_panel-App.test.js b/devtools/client/application/test/node/components/components_application_panel-App.test.js
new file mode 100644
index 0000000000..fb003be26e
--- /dev/null
+++ b/devtools/client/application/test/node/components/components_application_panel-App.test.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+// Import & init localization
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
+
+// Import component
+const App = createFactory(
+ require("resource://devtools/client/application/src/components/App.js")
+);
+
+describe("App", () => {
+ it("renders the expected snapshot", () => {
+ const wrapper = shallow(
+ LocalizationProvider({ bundles: [] }, App({}))
+ ).dive(); // dive to bypass the LocalizationProvider wrapper
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-Manifest.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-Manifest.test.js.snap
new file mode 100644
index 0000000000..d46eb63334
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-Manifest.test.js.snap
@@ -0,0 +1,396 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Manifest does not render the issues section when the manifest is valid 1`] = `
+<article
+ className="js-manifest"
+>
+ <Localized
+ id="manifest-view-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ManifestJsonLink />
+ <ManifestSection
+ key="manifest-section-1"
+ title="manifest-item-identity"
+ >
+ <table>
+ <tbody>
+ <ManifestItem
+ key="name"
+ label="name"
+ >
+ foo
+ </ManifestItem>
+ </tbody>
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-2"
+ title="manifest-item-presentation"
+ >
+ <table>
+ <tbody>
+ <ManifestItem
+ key="lorem"
+ label="lorem"
+ >
+ ipsum
+ </ManifestItem>
+ <ManifestItem
+ key="foo"
+ label="foo"
+ >
+ bar
+ </ManifestItem>
+ </tbody>
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-3"
+ title="manifest-item-icons"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+</article>
+`;
+
+exports[`Manifest does render the issues section when the manifest is not valid 1`] = `
+<article
+ className="js-manifest"
+>
+ <Localized
+ id="manifest-view-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ManifestJsonLink />
+ <ManifestSection
+ key="manifest-section-0"
+ title="manifest-item-warnings"
+ >
+ <ManifestIssueList
+ issues={
+ Array [
+ Object {
+ "level": "warning",
+ "message": "This is a warning",
+ },
+ ]
+ }
+ />
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-1"
+ title="manifest-item-identity"
+ >
+ <table>
+ <tbody>
+ <ManifestItem
+ key="name"
+ label="name"
+ >
+ foo
+ </ManifestItem>
+ </tbody>
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-2"
+ title="manifest-item-presentation"
+ >
+ <table>
+ <tbody>
+ <ManifestItem
+ key="lorem"
+ label="lorem"
+ >
+ ipsum
+ </ManifestItem>
+ <ManifestItem
+ key="foo"
+ label="foo"
+ >
+ bar
+ </ManifestItem>
+ </tbody>
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-3"
+ title="manifest-item-icons"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+</article>
+`;
+
+exports[`Manifest renders the expected snapshot for a manifest with color members 1`] = `
+<article
+ className="js-manifest"
+>
+ <Localized
+ id="manifest-view-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ManifestJsonLink />
+ <ManifestSection
+ key="manifest-section-1"
+ title="manifest-item-identity"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-2"
+ title="manifest-item-presentation"
+ >
+ <table>
+ <tbody>
+ <ManifestColorItem
+ key="background_color"
+ label="background_color"
+ value="red"
+ />
+ <ManifestColorItem
+ key="theme_color"
+ label="theme_color"
+ value="rgb(0, 0, 0)"
+ />
+ </tbody>
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-3"
+ title="manifest-item-icons"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+</article>
+`;
+
+exports[`Manifest renders the expected snapshot for a manifest with icon members 1`] = `
+<article
+ className="js-manifest"
+>
+ <Localized
+ id="manifest-view-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ManifestJsonLink />
+ <ManifestSection
+ key="manifest-section-1"
+ title="manifest-item-identity"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-2"
+ title="manifest-item-presentation"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-3"
+ title="manifest-item-icons"
+ >
+ <table>
+ <tbody>
+ <ManifestIconItem
+ key="0"
+ label={
+ Object {
+ "contentType": "image/png",
+ "sizes": "1x1",
+ }
+ }
+ value={
+ Object {
+ "purpose": "any",
+ "src": "something.png",
+ }
+ }
+ />
+ <ManifestIconItem
+ key="1"
+ label={
+ Object {
+ "contentType": "",
+ "sizes": "",
+ }
+ }
+ value={
+ Object {
+ "purpose": "any maskable",
+ "src": "something.svg",
+ }
+ }
+ />
+ </tbody>
+ </table>
+ </ManifestSection>
+</article>
+`;
+
+exports[`Manifest renders the expected snapshot for a manifest with string members 1`] = `
+<article
+ className="js-manifest"
+>
+ <Localized
+ id="manifest-view-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ManifestJsonLink />
+ <ManifestSection
+ key="manifest-section-1"
+ title="manifest-item-identity"
+ >
+ <table>
+ <tbody>
+ <ManifestItem
+ key="name"
+ label="name"
+ >
+ foo
+ </ManifestItem>
+ </tbody>
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-2"
+ title="manifest-item-presentation"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-3"
+ title="manifest-item-icons"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+</article>
+`;
+
+exports[`Manifest renders the expected snapshot for a manifest with unknown types 1`] = `
+<article
+ className="js-manifest"
+>
+ <Localized
+ id="manifest-view-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ManifestJsonLink />
+ <ManifestSection
+ key="manifest-section-1"
+ title="manifest-item-identity"
+ >
+ <table>
+ <tbody>
+ <ManifestItem
+ key="lorem"
+ label="lorem"
+ >
+ ipsum
+ </ManifestItem>
+ </tbody>
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-2"
+ title="manifest-item-presentation"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-3"
+ title="manifest-item-icons"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+</article>
+`;
+
+exports[`Manifest renders the expected snapshot for a manifest with url members 1`] = `
+<article
+ className="js-manifest"
+>
+ <Localized
+ id="manifest-view-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ManifestJsonLink />
+ <ManifestSection
+ key="manifest-section-1"
+ title="manifest-item-identity"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-2"
+ title="manifest-item-presentation"
+ >
+ <table>
+ <tbody>
+ <ManifestUrlItem
+ key="start_url"
+ label="start_url"
+ value="https://example.com/"
+ />
+ <ManifestUrlItem
+ key="scope"
+ label="scope"
+ value="https://example.com/"
+ />
+ </tbody>
+ </table>
+ </ManifestSection>
+ <ManifestSection
+ key="manifest-section-3"
+ title="manifest-item-icons"
+ >
+ <table>
+ <tbody />
+ </table>
+ </ManifestSection>
+</article>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestColorItem.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestColorItem.test.js.snap
new file mode 100644
index 0000000000..4f3e485084
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestColorItem.test.js.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestColorItem does not strip translucent alpha from the displayed color 1`] = `
+<ManifestItem
+ label="foo"
+>
+ <div
+ className="manifest-item__color"
+ style={
+ Object {
+ "--color-value": "#00FF00FA",
+ }
+ }
+ >
+ #00FF00FA
+ </div>
+</ManifestItem>
+`;
+
+exports[`ManifestColorItem renders the expected snapshot for a populated color item 1`] = `
+<ManifestItem
+ label="foo"
+>
+ <div
+ className="manifest-item__color"
+ style={
+ Object {
+ "--color-value": "#ff0000",
+ }
+ }
+ >
+ #ff0000
+ </div>
+</ManifestItem>
+`;
+
+exports[`ManifestColorItem renders the expected snapshot for an empty color item 1`] = `
+<ManifestItem
+ label="foo"
+/>
+`;
+
+exports[`ManifestColorItem strips opaque alpha from the displayed color 1`] = `
+<ManifestItem
+ label="foo"
+>
+ <div
+ className="manifest-item__color"
+ style={
+ Object {
+ "--color-value": "#00FF00",
+ }
+ }
+ >
+ #00FF00
+ </div>
+</ManifestItem>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestEmpty.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestEmpty.test.js.snap
new file mode 100644
index 0000000000..bdc5e9ed60
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestEmpty.test.js.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestEmpty renders the expected snapshot 1`] = `
+<article
+ className="app-page__icon-container js-manifest-empty"
+>
+ <aside>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ }
+ }
+ id="sidebar-item-manifest"
+ >
+ <img
+ className="app-page__icon"
+ src="chrome://devtools/skin/images/application-manifest.svg"
+ />
+ </Localized>
+ </aside>
+ <div>
+ <Localized
+ id="manifest-empty-intro2"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <p>
+ <Localized
+ id="manifest-empty-intro-link"
+ >
+ <a
+ onClick={[Function]}
+ />
+ </Localized>
+ </p>
+ <Localized
+ id="manifest-non-existing"
+ >
+ <p />
+ </Localized>
+ </div>
+</article>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIconItem.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIconItem.test.js.snap
new file mode 100644
index 0000000000..200c6306de
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIconItem.test.js.snap
@@ -0,0 +1,106 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestIconItem renders the expected snapshop when a label member is missing 1`] = `
+<ManifestItem
+ label={
+ Array [
+ null,
+ null,
+ "image/png",
+ ]
+ }
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ }
+ }
+ id="manifest-icon-img"
+ >
+ <img
+ className="manifest-item__icon"
+ src="icon.png"
+ title="manifest-icon-img-title-no-sizes"
+ />
+ </Localized>
+ <br />
+ <Localized
+ $purpose="any"
+ code={<code />}
+ id="manifest-icon-purpose"
+ >
+ <span />
+ </Localized>
+</ManifestItem>
+`;
+
+exports[`ManifestIconItem renders the expected snapshop when all label members are missing 1`] = `
+<ManifestItem
+ label={
+ Array [
+ null,
+ null,
+ null,
+ ]
+ }
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ }
+ }
+ id="manifest-icon-img"
+ >
+ <img
+ className="manifest-item__icon"
+ src="icon.png"
+ title="manifest-icon-img-title-no-sizes"
+ />
+ </Localized>
+ <br />
+ <Localized
+ $purpose="any"
+ code={<code />}
+ id="manifest-icon-purpose"
+ >
+ <span />
+ </Localized>
+</ManifestItem>
+`;
+
+exports[`ManifestIconItem renders the expected snapshot for a fully populated icon item 1`] = `
+<ManifestItem
+ label={
+ Array [
+ "128x128",
+ <br />,
+ "image/png",
+ ]
+ }
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ }
+ }
+ id="manifest-icon-img"
+ >
+ <img
+ className="manifest-item__icon"
+ src="icon.png"
+ title="manifest-icon-img-title__{\\"sizes\\":\\"128x128\\"}"
+ />
+ </Localized>
+ <br />
+ <Localized
+ $purpose="any"
+ code={<code />}
+ id="manifest-icon-purpose"
+ >
+ <span />
+ </Localized>
+</ManifestItem>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIssue.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIssue.test.js.snap
new file mode 100644
index 0000000000..99e83af1f2
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIssue.test.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestIssue renders the expected snapshot for a warning 1`] = `
+<li
+ className="js-manifest-issue "
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ "title": true,
+ }
+ }
+ id="icon-warning"
+ >
+ <img
+ className="manifest-issue__icon manifest-issue__icon--warning"
+ src="chrome://devtools/skin/images/alert-small.svg"
+ />
+ </Localized>
+ <span>
+ Lorem ipsum
+ </span>
+</li>
+`;
+
+exports[`ManifestIssue renders the expected snapshot for an error 1`] = `
+<li
+ className="js-manifest-issue "
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ "title": true,
+ }
+ }
+ id="icon-error"
+ >
+ <img
+ className="manifest-issue__icon manifest-issue__icon--error"
+ src="resource://devtools-shared-images/error-small.svg"
+ />
+ </Localized>
+ <span>
+ Lorem ipsum
+ </span>
+</li>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIssueList.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIssueList.test.js.snap
new file mode 100644
index 0000000000..edbf2d07c9
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestIssueList.test.js.snap
@@ -0,0 +1,89 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestIssueList groups issues by level and shows errors first 1`] = `
+Array [
+ <ul
+ className="manifest-issues js-manifest-issues"
+ key="issuelist-0"
+ >
+ <ManifestIssue
+ className="manifest-issues__item"
+ key="issue-0"
+ level="error"
+ message="An error"
+ />
+ </ul>,
+ <ul
+ className="manifest-issues js-manifest-issues"
+ key="issuelist-1"
+ >
+ <ManifestIssue
+ className="manifest-issues__item"
+ key="issue-0"
+ level="warning"
+ message="A warning"
+ />
+ <ManifestIssue
+ className="manifest-issues__item"
+ key="issue-1"
+ level="warning"
+ message="Another warning"
+ />
+ </ul>,
+]
+`;
+
+exports[`ManifestIssueList renders nothing for empty issues 1`] = `null`;
+
+exports[`ManifestIssueList renders the expected snapshot for a populated list 1`] = `
+Array [
+ <ul
+ className="manifest-issues js-manifest-issues"
+ key="issuelist-0"
+ >
+ <ManifestIssue
+ className="manifest-issues__item"
+ key="issue-0"
+ level="error"
+ message="Foo"
+ />
+ </ul>,
+ <ul
+ className="manifest-issues js-manifest-issues"
+ key="issuelist-1"
+ >
+ <ManifestIssue
+ className="manifest-issues__item"
+ key="issue-0"
+ level="warning"
+ message="Foo"
+ />
+ <ManifestIssue
+ className="manifest-issues__item"
+ key="issue-1"
+ level="warning"
+ message="Bar"
+ />
+ </ul>,
+]
+`;
+
+exports[`ManifestIssueList skips rendering empty level groups 1`] = `
+<ul
+ className="manifest-issues js-manifest-issues"
+ key="issuelist-0"
+>
+ <ManifestIssue
+ className="manifest-issues__item"
+ key="issue-0"
+ level="warning"
+ message="A warning"
+ />
+ <ManifestIssue
+ className="manifest-issues__item"
+ key="issue-1"
+ level="warning"
+ message="Another warning"
+ />
+</ul>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestItem.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestItem.test.js.snap
new file mode 100644
index 0000000000..69d983d308
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestItem.test.js.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestItem renders the expected snapshot for a populated item 1`] = `
+<tr
+ className="manifest-item js-manifest-item"
+>
+ <th
+ className="manifest-item__label js-manifest-item-label"
+ scope="row"
+ >
+ foo
+ </th>
+ <td
+ className="manifest-item__value js-manifest-item-content"
+ >
+ bar
+ </td>
+</tr>
+`;
+
+exports[`ManifestItem renders the expected snapshot for an empty item 1`] = `
+<tr
+ className="manifest-item js-manifest-item"
+>
+ <th
+ className="manifest-item__label js-manifest-item-label"
+ scope="row"
+ >
+ foo
+ </th>
+ <td
+ className="manifest-item__value js-manifest-item-content"
+ />
+</tr>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestJsonLink.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestJsonLink.test.js.snap
new file mode 100644
index 0000000000..061578b846
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestJsonLink.test.js.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestJsonLink renders the expected snapshot when given a data URL 1`] = `
+<p
+ className="manifest-json-link"
+>
+ <Localized
+ id="manifest-json-link-data-url"
+ />
+</p>
+`;
+
+exports[`ManifestJsonLink renders the expected snapshot when given a regular URL 1`] = `
+<p
+ className="manifest-json-link"
+>
+ <a
+ className="js-manifest-json-link devtools-ellipsis-text"
+ href="#"
+ onClick={[Function]}
+ title="https://example.com/manifest.json"
+ >
+ https://example.com/manifest.json
+ </a>
+</p>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestLoader.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestLoader.test.js.snap
new file mode 100644
index 0000000000..16c885cf80
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestLoader.test.js.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestLoader renders a message when it is loading 1`] = `
+<aside
+ className="manifest-loader"
+>
+ <Localized
+ id="manifest-loading"
+ >
+ <p
+ className="manifest-loader__load js-manifest-loading"
+ />
+ </Localized>
+</aside>
+`;
+
+exports[`ManifestLoader renders a message when manifest has failed to load 1`] = `
+<aside
+ className="manifest-loader"
+>
+ <Localized
+ id="manifest-loaded-error"
+ key="manifest-error-label"
+ >
+ <h1
+ className="js-manifest-loaded-error app-page__title"
+ />
+ </Localized>
+ <p
+ className="technical-text"
+ key="manifest-error-message"
+ >
+ lorem ipsum
+ </p>
+</aside>
+`;
+
+exports[`ManifestLoader renders a message when manifest has loaded OK 1`] = `
+<aside
+ className="manifest-loader"
+>
+ <Localized
+ id="manifest-loaded-ok"
+ >
+ <p
+ className="js-manifest-loaded-ok"
+ />
+ </Localized>
+</aside>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestPage.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestPage.test.js.snap
new file mode 100644
index 0000000000..4700ccf935
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestPage.test.js.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestPage renders the expected snapshot when the manifest is loading 1`] = `
+<section
+ className="app-page js-manifest-page app-page--empty"
+>
+ <Connect(ManifestLoader) />
+</section>
+`;
+
+exports[`ManifestPage renders the expected snapshot when the manifest needs to load 1`] = `
+<section
+ className="app-page js-manifest-page app-page--empty"
+>
+ <Connect(ManifestLoader) />
+</section>
+`;
+
+exports[`ManifestPage renders the expected snapshot when there is a manifest 1`] = `
+<section
+ className="app-page js-manifest-page "
+>
+ <Manifest
+ icons={
+ Array [
+ Object {
+ "key": Object {
+ "contentType": "image/png",
+ "sizes": "1x1",
+ },
+ "type": "icon",
+ "value": Object {
+ "purpose": "any",
+ "src": "something.png",
+ },
+ },
+ ]
+ }
+ identity={
+ Array [
+ Object {
+ "key": "name",
+ "type": "string",
+ "value": "foo",
+ },
+ ]
+ }
+ presentation={
+ Array [
+ Object {
+ "key": "lorem",
+ "type": "string",
+ "value": "ipsum",
+ },
+ Object {
+ "key": "foo",
+ "type": "string",
+ "value": "bar",
+ },
+ ]
+ }
+ validation={
+ Array [
+ Object {
+ "level": "warning",
+ "message": "This is a warning",
+ },
+ ]
+ }
+ />
+</section>
+`;
+
+exports[`ManifestPage renders the expected snapshot when there is no manifest 1`] = `
+<section
+ className="app-page js-manifest-page app-page--empty"
+>
+ <ManifestEmpty />
+</section>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestSection.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestSection.test.js.snap
new file mode 100644
index 0000000000..e8ea10867f
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestSection.test.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestSection renders the expected snapshot for a populated section 1`] = `
+<section
+ className="manifest-section "
+>
+ <h2
+ className="manifest-section__title"
+ >
+ Lorem ipsum
+ </h2>
+ <tr>
+ <td>
+ foo
+ </td>
+ </tr>
+</section>
+`;
+
+exports[`ManifestSection renders the expected snapshot for a section with no children 1`] = `
+<section
+ className="manifest-section manifest-section--empty"
+>
+ <h2
+ className="manifest-section__title"
+ >
+ Lorem ipsum
+ </h2>
+</section>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestUrlItem.test.js.snap b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestUrlItem.test.js.snap
new file mode 100644
index 0000000000..62f4fdfe11
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/__snapshots__/components_application_panel-ManifestUrlItem.test.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ManifestUrlItem renders the expected snapshot for a populated url 1`] = `
+<ManifestItem
+ label="foo"
+>
+ <div
+ className="manifest-item__url"
+ />
+</ManifestItem>
+`;
+
+exports[`ManifestUrlItem renders the expected snapshot for an empty url 1`] = `
+<ManifestItem
+ label="foo"
+>
+ <div
+ className="manifest-item__url"
+ />
+</ManifestItem>
+`;
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-Manifest.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-Manifest.test.js
new file mode 100644
index 0000000000..0b554fbdd2
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-Manifest.test.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const Manifest = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/Manifest.js")
+);
+
+const {
+ MANIFEST_COLOR_MEMBERS,
+ MANIFEST_ICON_MEMBERS,
+ MANIFEST_STRING_MEMBERS,
+ MANIFEST_UNKNOWN_TYPE_MEMBERS,
+ MANIFEST_URL_MEMBERS,
+ MANIFEST_NO_ISSUES,
+ MANIFEST_WITH_ISSUES,
+} = require("resource://devtools/client/application/test/node/fixtures/data/constants.js");
+
+/*
+ * Test for Manifest component
+ */
+
+describe("Manifest", () => {
+ it("renders the expected snapshot for a manifest with string members", () => {
+ const wrapper = shallow(Manifest(MANIFEST_STRING_MEMBERS));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for a manifest with color members", () => {
+ const wrapper = shallow(Manifest(MANIFEST_COLOR_MEMBERS));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for a manifest with unknown types", () => {
+ const wrapper = shallow(Manifest(MANIFEST_UNKNOWN_TYPE_MEMBERS));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for a manifest with icon members", () => {
+ const wrapper = shallow(Manifest(MANIFEST_ICON_MEMBERS));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for a manifest with url members", () => {
+ const wrapper = shallow(Manifest(MANIFEST_URL_MEMBERS));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("does render the issues section when the manifest is not valid", () => {
+ const wrapper = shallow(Manifest(MANIFEST_WITH_ISSUES));
+ expect(wrapper).toMatchSnapshot();
+
+ const sections = wrapper.find("ManifestSection");
+ expect(sections).toHaveLength(4);
+ expect(sections.get(0).props.title).toBe("manifest-item-warnings");
+ expect(sections.find("ManifestIssueList")).toHaveLength(1);
+ });
+
+ it("does not render the issues section when the manifest is valid", () => {
+ const wrapper = shallow(Manifest(MANIFEST_NO_ISSUES));
+ expect(wrapper).toMatchSnapshot();
+
+ const sections = wrapper.find("ManifestSection");
+ expect(sections).toHaveLength(3);
+ expect(sections.get(0).props.title).not.toBe("manifest-item-warnings");
+ expect(sections.find("ManifestIssueList")).toHaveLength(0);
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestColorItem.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestColorItem.test.js
new file mode 100644
index 0000000000..eac67ec195
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestColorItem.test.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const ManifestColorItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestColorItem.js")
+);
+
+/*
+ * Unit tests for the ManifestItem component
+ */
+
+describe("ManifestColorItem", () => {
+ it("renders the expected snapshot for a populated color item", () => {
+ const wrapper = shallow(
+ ManifestColorItem({ label: "foo", value: "#ff0000" })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for an empty color item", () => {
+ const wrapper = shallow(ManifestColorItem({ label: "foo" }));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("strips opaque alpha from the displayed color", () => {
+ const wrapper = shallow(
+ ManifestColorItem({ label: "foo", value: "#00FF00FF" })
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ expect(wrapper.find(".manifest-item__color").text()).toBe("#00FF00");
+ });
+
+ it("does not strip translucent alpha from the displayed color", () => {
+ const wrapper = shallow(
+ ManifestColorItem({ label: "foo", value: "#00FF00FA" })
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ expect(wrapper.find(".manifest-item__color").text()).toBe("#00FF00FA");
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestEmpty.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestEmpty.test.js
new file mode 100644
index 0000000000..de27a56ec5
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestEmpty.test.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const ManifestEmpty = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestEmpty.js")
+);
+
+/**
+ * Test for ManifestEmpty component
+ */
+
+describe("ManifestEmpty", () => {
+ it("renders the expected snapshot", () => {
+ const wrapper = shallow(ManifestEmpty({}));
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIconItem.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIconItem.test.js
new file mode 100644
index 0000000000..e3ea293c82
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIconItem.test.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const ManifestIconItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIconItem.js")
+);
+
+/*
+ * Unit tests for the ManifestIconItem component
+ */
+
+describe("ManifestIconItem", () => {
+ it("renders the expected snapshot for a fully populated icon item", () => {
+ const wrapper = shallow(
+ ManifestIconItem({
+ label: { sizes: "128x128", contentType: "image/png" },
+ value: { src: "icon.png", purpose: "any" },
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshop when a label member is missing", () => {
+ const wrapper = shallow(
+ ManifestIconItem({
+ label: { sizes: undefined, contentType: "image/png" },
+ value: { src: "icon.png", purpose: "any" },
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshop when all label members are missing", () => {
+ const wrapper = shallow(
+ ManifestIconItem({
+ label: { sizes: undefined, contentType: undefined },
+ value: { src: "icon.png", purpose: "any" },
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIssue.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIssue.test.js
new file mode 100644
index 0000000000..41350557be
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIssue.test.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const ManifestIssue = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIssue.js")
+);
+
+/*
+ * Tests for the ManifestIssue component
+ */
+
+describe("ManifestIssue", () => {
+ it("renders the expected snapshot for a warning", () => {
+ const issue = { level: "warning", message: "Lorem ipsum" };
+ const wrapper = shallow(ManifestIssue(issue));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for an error", () => {
+ const issue = { level: "error", message: "Lorem ipsum" };
+ const wrapper = shallow(ManifestIssue(issue));
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIssueList.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIssueList.test.js
new file mode 100644
index 0000000000..71d59d3e07
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestIssueList.test.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const ManifestIssueList = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIssueList.js")
+);
+
+/*
+ * Tests for the ManifestIssue component
+ */
+
+describe("ManifestIssueList", () => {
+ it("renders the expected snapshot for a populated list", () => {
+ const issues = [
+ { level: "error", message: "Foo" },
+ { level: "warning", message: "Foo" },
+ { level: "warning", message: "Bar" },
+ ];
+ const wrapper = shallow(ManifestIssueList({ issues }));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("groups issues by level and shows errors first", () => {
+ const issues = [
+ { level: "warning", message: "A warning" },
+ { level: "error", message: "An error" },
+ { level: "warning", message: "Another warning" },
+ ];
+ const wrapper = shallow(ManifestIssueList({ issues }));
+ expect(wrapper).toMatchSnapshot();
+
+ expect(wrapper.find("ManifestIssue").get(0).props.level).toBe("error");
+ expect(wrapper.find("ManifestIssue").get(1).props.level).toBe("warning");
+ expect(wrapper.find("ManifestIssue").get(2).props.level).toBe("warning");
+ });
+
+ it("skips rendering empty level groups", () => {
+ const issues = [
+ { level: "warning", message: "A warning" },
+ { level: "warning", message: "Another warning" },
+ ];
+ const wrapper = shallow(ManifestIssueList({ issues }));
+ expect(wrapper).toMatchSnapshot();
+
+ const lists = wrapper.find(".js-manifest-issues");
+ expect(lists).toHaveLength(1);
+ });
+
+ it("renders nothing for empty issues", () => {
+ const wrapper = shallow(ManifestIssueList({ issues: [] }));
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestItem.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestItem.test.js
new file mode 100644
index 0000000000..ca9303aab4
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestItem.test.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+
+/*
+ * Unit tests for the ManifestItem component
+ */
+
+describe("ManifestItem", () => {
+ it("renders the expected snapshot for a populated item", () => {
+ const wrapper = shallow(ManifestItem({ label: "foo" }, "bar"));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for an empty item", () => {
+ const wrapper = shallow(ManifestItem({ label: "foo" }));
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestJsonLink.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestJsonLink.test.js
new file mode 100644
index 0000000000..fccab31b9d
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestJsonLink.test.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const ManifestJsonLink = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestJsonLink.js")
+);
+
+/*
+ * Test for the ManifestJsonLink component
+ */
+
+describe("ManifestJsonLink", () => {
+ it("renders the expected snapshot when given a regular URL", () => {
+ const wrapper = shallow(
+ ManifestJsonLink({ url: "https://example.com/manifest.json" })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when given a data URL", () => {
+ const wrapper = shallow(
+ ManifestJsonLink({
+ url: `data:application/manifest+json,{"name": "Foo"}`,
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ // assert there's no link for data URLs
+ expect(wrapper.find("a").length).toBe(0);
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestLoader.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestLoader.test.js
new file mode 100644
index 0000000000..d3cc8595ce
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestLoader.test.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+// Import test helpers
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+// Import fixtures
+const {
+ MANIFEST_NO_ISSUES,
+} = require("resource://devtools/client/application/test/node/fixtures/data/constants.js");
+
+const manifestActions = require("resource://devtools/client/application/src/actions/manifest.js");
+// NOTE: we need to spy on the action before we load the component, so it gets
+// bound to the spy, not the original implementation
+const fetchManifestActionSpy = jest.spyOn(manifestActions, "fetchManifest");
+
+const ManifestLoader = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestLoader.js")
+);
+
+describe("ManifestLoader", () => {
+ function buildStore({ manifest, errorMessage, isLoading }) {
+ const manifestState = Object.assign(
+ {
+ manifest: null,
+ errorMessage: "",
+ isLoading: false,
+ },
+ { manifest, errorMessage, isLoading }
+ );
+
+ return setupStore({ manifest: manifestState });
+ }
+
+ afterAll(() => {
+ fetchManifestActionSpy.mockRestore();
+ });
+
+ it("loads a manifest when mounted", async () => {
+ fetchManifestActionSpy.mockReturnValue({ type: "foo" });
+
+ const store = buildStore({});
+
+ shallow(ManifestLoader({ store })).dive();
+
+ expect(manifestActions.fetchManifest).toHaveBeenCalled();
+ fetchManifestActionSpy.mockReset();
+ });
+
+ it("renders a message when it is loading", async () => {
+ const store = buildStore({ isLoading: true });
+ const wrapper = shallow(ManifestLoader({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders a message when manifest has loaded OK", async () => {
+ const store = buildStore({
+ isLoading: false,
+ manifest: MANIFEST_NO_ISSUES,
+ errorMessage: "",
+ });
+ const wrapper = shallow(ManifestLoader({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders a message when manifest has failed to load", async () => {
+ const store = buildStore({
+ manifest: null,
+ isLoading: false,
+ errorMessage: "lorem ipsum",
+ });
+ const wrapper = shallow(ManifestLoader({ store })).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestPage.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestPage.test.js
new file mode 100644
index 0000000000..b479882924
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestPage.test.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+const {
+ MANIFEST_SIMPLE,
+} = require("resource://devtools/client/application/test/node/fixtures/data/constants.js");
+
+const ManifestPage = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestPage.js")
+);
+
+/**
+ * Test for ManifestPage.js component
+ */
+
+describe("ManifestPage", () => {
+ function buildStoreWithManifest(manifest, isLoading = false) {
+ return setupStore({
+ manifest: {
+ manifest,
+ errorMessage: "",
+ isLoading,
+ },
+ });
+ }
+
+ it("renders the expected snapshot when there is a manifest", () => {
+ const store = buildStoreWithManifest(MANIFEST_SIMPLE);
+ const wrapper = shallow(ManifestPage({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when the manifest needs to load", () => {
+ const store = buildStoreWithManifest(undefined);
+ const wrapper = shallow(ManifestPage({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when the manifest is loading", () => {
+ const store = buildStoreWithManifest(undefined, true);
+ const wrapper = shallow(ManifestPage({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when there is no manifest", () => {
+ const store = buildStoreWithManifest(null);
+ const wrapper = shallow(ManifestPage({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestSection.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestSection.test.js
new file mode 100644
index 0000000000..c117febb57
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestSection.test.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+const {
+ td,
+ tr,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const ManifestSection = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestSection.js")
+);
+
+/*
+ * Unit tests for the ManifestSection component
+ */
+
+describe("ManifestSection", () => {
+ it("renders the expected snapshot for a populated section", () => {
+ const content = tr({}, td({}, "foo"));
+ const wrapper = shallow(ManifestSection({ title: "Lorem ipsum" }, content));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for a section with no children", () => {
+ const wrapper = shallow(ManifestSection({ title: "Lorem ipsum" }));
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find(".manifest-section--empty"));
+ });
+});
diff --git a/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestUrlItem.test.js b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestUrlItem.test.js
new file mode 100644
index 0000000000..1ef1504a3f
--- /dev/null
+++ b/devtools/client/application/test/node/components/manifest/components_application_panel-ManifestUrlItem.test.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const ManifestUrlItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestUrlItem.js")
+);
+
+/*
+ * Unit tests for the ManifestUrlItem component
+ */
+
+describe("ManifestUrlItem", () => {
+ it("renders the expected snapshot for a populated url", () => {
+ const wrapper = shallow(
+ ManifestUrlItem({ label: "foo" }, "https://example.com")
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for an empty url", () => {
+ const wrapper = shallow(ManifestUrlItem({ label: "foo" }));
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-PageSwitcher.test.js.snap b/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-PageSwitcher.test.js.snap
new file mode 100644
index 0000000000..4fc899a511
--- /dev/null
+++ b/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-PageSwitcher.test.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PageSwitcher renders nothing when an invalid page is selected 1`] = `""`;
+
+exports[`PageSwitcher renders nothing when no page is selected 1`] = `""`;
+
+exports[`PageSwitcher renders the ManifestPage component when manifest page is selected 1`] = `<Connect(ManifestPage) />`;
+
+exports[`PageSwitcher renders the WorkersPage component when workers page is selected 1`] = `<Connect(WorkersPage) />`;
diff --git a/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-Sidebar.test.js.snap b/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-Sidebar.test.js.snap
new file mode 100644
index 0000000000..4348d7c5b9
--- /dev/null
+++ b/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-Sidebar.test.js.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Sidebar renders the expected snapshot when no page is selected 1`] = `
+<aside
+ className="sidebar js-sidebar"
+>
+ <ul
+ className="sidebar__list"
+ >
+ <Connect(SidebarItem)
+ isSelected={false}
+ key="sidebar-item-service-workers"
+ page="service-workers"
+ />
+ <Connect(SidebarItem)
+ isSelected={false}
+ key="sidebar-item-manifest"
+ page="manifest"
+ />
+ </ul>
+</aside>
+`;
+
+exports[`Sidebar renders the expected snapshot when the manifest page is selected 1`] = `
+<aside
+ className="sidebar js-sidebar"
+>
+ <ul
+ className="sidebar__list"
+ >
+ <Connect(SidebarItem)
+ isSelected={false}
+ key="sidebar-item-service-workers"
+ page="service-workers"
+ />
+ <Connect(SidebarItem)
+ isSelected={true}
+ key="sidebar-item-manifest"
+ page="manifest"
+ />
+ </ul>
+</aside>
+`;
+
+exports[`Sidebar renders the expected snapshot when the service workers page is selected 1`] = `
+<aside
+ className="sidebar js-sidebar"
+>
+ <ul
+ className="sidebar__list"
+ >
+ <Connect(SidebarItem)
+ isSelected={true}
+ key="sidebar-item-service-workers"
+ page="service-workers"
+ />
+ <Connect(SidebarItem)
+ isSelected={false}
+ key="sidebar-item-manifest"
+ page="manifest"
+ />
+ </ul>
+</aside>
+`;
diff --git a/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-SidebarItem.test.js.snap b/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-SidebarItem.test.js.snap
new file mode 100644
index 0000000000..c5f3122e6c
--- /dev/null
+++ b/devtools/client/application/test/node/components/routing/__snapshots__/components_application_panel-SidebarItem.test.js.snap
@@ -0,0 +1,141 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SidebarItem renders the expected snapshot when the manifest page is not selected 1`] = `
+<li
+ className="sidebar-item js-sidebar-manifest "
+ onClick={[Function]}
+ role="link"
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ "title": true,
+ }
+ }
+ id="sidebar-item-manifest"
+ >
+ <img
+ className="sidebar-item__icon"
+ src="chrome://devtools/skin/images/application-manifest.svg"
+ />
+ </Localized>
+ <Localized
+ attrs={
+ Object {
+ "title": true,
+ }
+ }
+ id="sidebar-item-manifest"
+ >
+ <span
+ className="devtools-ellipsis-text"
+ />
+ </Localized>
+</li>
+`;
+
+exports[`SidebarItem renders the expected snapshot when the manifest page is selected 1`] = `
+<li
+ className="sidebar-item js-sidebar-manifest sidebar-item--selected"
+ onClick={[Function]}
+ role="link"
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ "title": true,
+ }
+ }
+ id="sidebar-item-manifest"
+ >
+ <img
+ className="sidebar-item__icon"
+ src="chrome://devtools/skin/images/application-manifest.svg"
+ />
+ </Localized>
+ <Localized
+ attrs={
+ Object {
+ "title": true,
+ }
+ }
+ id="sidebar-item-manifest"
+ >
+ <span
+ className="devtools-ellipsis-text"
+ />
+ </Localized>
+</li>
+`;
+
+exports[`SidebarItem renders the expected snapshot when the service-workers page is not selected 1`] = `
+<li
+ className="sidebar-item js-sidebar-service-workers "
+ onClick={[Function]}
+ role="link"
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ "title": true,
+ }
+ }
+ id="sidebar-item-service-workers"
+ >
+ <img
+ className="sidebar-item__icon"
+ src="chrome://devtools/skin/images/debugging-workers.svg"
+ />
+ </Localized>
+ <Localized
+ attrs={
+ Object {
+ "title": true,
+ }
+ }
+ id="sidebar-item-service-workers"
+ >
+ <span
+ className="devtools-ellipsis-text"
+ />
+ </Localized>
+</li>
+`;
+
+exports[`SidebarItem renders the expected snapshot when the service-workers page is selected 1`] = `
+<li
+ className="sidebar-item js-sidebar-service-workers sidebar-item--selected"
+ onClick={[Function]}
+ role="link"
+>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ "title": true,
+ }
+ }
+ id="sidebar-item-service-workers"
+ >
+ <img
+ className="sidebar-item__icon"
+ src="chrome://devtools/skin/images/debugging-workers.svg"
+ />
+ </Localized>
+ <Localized
+ attrs={
+ Object {
+ "title": true,
+ }
+ }
+ id="sidebar-item-service-workers"
+ >
+ <span
+ className="devtools-ellipsis-text"
+ />
+ </Localized>
+</li>
+`;
diff --git a/devtools/client/application/test/node/components/routing/components_application_panel-PageSwitcher.test.js b/devtools/client/application/test/node/components/routing/components_application_panel-PageSwitcher.test.js
new file mode 100644
index 0000000000..1e69aa6869
--- /dev/null
+++ b/devtools/client/application/test/node/components/routing/components_application_panel-PageSwitcher.test.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+// Import setupStore with imported & combined reducers
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+
+const PageSwitcher = createFactory(
+ require("resource://devtools/client/application/src/components/routing/PageSwitcher.js")
+);
+
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+/**
+ * Test for workerListEmpty.js component
+ */
+
+describe("PageSwitcher", () => {
+ function buildStoreWithSelectedPage(selectedPage) {
+ return setupStore({
+ ui: {
+ selectedPage,
+ },
+ });
+ }
+
+ const consoleErrorSpy = jest
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
+
+ beforeEach(() => {
+ console.error.mockClear();
+ });
+
+ afterAll(() => {
+ consoleErrorSpy.mockRestore();
+ });
+
+ it("renders the ManifestPage component when manifest page is selected", () => {
+ const store = buildStoreWithSelectedPage(PAGE_TYPES.MANIFEST);
+ const wrapper = shallow(PageSwitcher({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the WorkersPage component when workers page is selected", () => {
+ const store = buildStoreWithSelectedPage(PAGE_TYPES.SERVICE_WORKERS);
+ const wrapper = shallow(PageSwitcher({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders nothing when no page is selected", () => {
+ const store = buildStoreWithSelectedPage(null);
+ const wrapper = shallow(PageSwitcher({ store })).dive();
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders nothing when an invalid page is selected", () => {
+ const store = buildStoreWithSelectedPage("foo");
+ const wrapper = shallow(PageSwitcher({ store })).dive();
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/routing/components_application_panel-Sidebar.test.js b/devtools/client/application/test/node/components/routing/components_application_panel-Sidebar.test.js
new file mode 100644
index 0000000000..4593f702bc
--- /dev/null
+++ b/devtools/client/application/test/node/components/routing/components_application_panel-Sidebar.test.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const Sidebar = createFactory(
+ require("resource://devtools/client/application/src/components/routing/Sidebar.js")
+);
+
+/**
+ * Test for Sidebar.js component
+ */
+
+describe("Sidebar", () => {
+ function buildStoreWithSelectedPage(selectedPage) {
+ return setupStore({
+ ui: {
+ selectedPage,
+ },
+ });
+ }
+ it("renders the expected snapshot when the manifest page is selected", () => {
+ const store = buildStoreWithSelectedPage(PAGE_TYPES.MANIFEST);
+ const wrapper = shallow(Sidebar({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when the service workers page is selected", () => {
+ const store = buildStoreWithSelectedPage(PAGE_TYPES.SERVICE_WORKERS);
+ const wrapper = shallow(Sidebar({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when no page is selected", () => {
+ const store = buildStoreWithSelectedPage();
+ const wrapper = shallow(Sidebar({ store })).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/routing/components_application_panel-SidebarItem.test.js b/devtools/client/application/test/node/components/routing/components_application_panel-SidebarItem.test.js
new file mode 100644
index 0000000000..955c97acd2
--- /dev/null
+++ b/devtools/client/application/test/node/components/routing/components_application_panel-SidebarItem.test.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const SidebarItem = createFactory(
+ require("resource://devtools/client/application/src/components/routing/SidebarItem.js")
+);
+
+/**
+ * Test for SidebarItem.js component
+ */
+
+describe("SidebarItem", () => {
+ function buildStoreWithSelectedPage(selectedPage) {
+ return setupStore({
+ ui: {
+ selectedPage,
+ },
+ });
+ }
+
+ it("renders the expected snapshot when the manifest page is selected", () => {
+ const store = buildStoreWithSelectedPage(PAGE_TYPES.MANIFEST);
+ const wrapper = shallow(
+ SidebarItem({
+ store,
+ page: "manifest",
+ isSelected: true,
+ })
+ ).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when the service-workers page is selected", () => {
+ const store = buildStoreWithSelectedPage(PAGE_TYPES.SERVICE_WORKERS);
+ const wrapper = shallow(
+ SidebarItem({
+ store,
+ isSelected: true,
+ page: "service-workers",
+ })
+ ).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when the manifest page is not selected", () => {
+ const store = buildStoreWithSelectedPage(PAGE_TYPES.MANIFEST);
+ const wrapper = shallow(
+ SidebarItem({
+ store,
+ isSelected: false,
+ page: "manifest",
+ })
+ ).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot when the service-workers page is not selected", () => {
+ const store = buildStoreWithSelectedPage(PAGE_TYPES.SERVICE_WORKERS);
+ const wrapper = shallow(
+ SidebarItem({
+ store,
+ isSelected: false,
+ page: "service-workers",
+ })
+ ).dive();
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-Registration.test.js.snap b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-Registration.test.js.snap
new file mode 100644
index 0000000000..f85bf8b2a1
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-Registration.test.js.snap
@@ -0,0 +1,180 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registration Renders the expected snapshot for a registration with a worker 1`] = `
+<li
+ className=""
+>
+ <article
+ className="registration js-sw-container"
+ >
+ <header
+ className="registration__header"
+ >
+ <h2
+ className="registration__scope js-sw-scope devtools-ellipsis-text"
+ title="SCOPE 123"
+ >
+ SCOPE 123
+ </h2>
+ </header>
+ <aside
+ className="registration__controls"
+ >
+ <Localized
+ id="serviceworker-worker-unregister"
+ >
+ <UIButton
+ className="js-unregister-button"
+ onClick={[Function]}
+ />
+ </Localized>
+ </aside>
+ <ul
+ className="registration__workers"
+ >
+ <li
+ className="registration__workers-item"
+ key="id-worker-1-example"
+ >
+ <Connect(Worker)
+ isDebugEnabled={true}
+ worker={
+ Object {
+ "id": "id-worker-1-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ }
+ }
+ />
+ </li>
+ </ul>
+ </article>
+</li>
+`;
+
+exports[`Registration Renders the expected snapshot for a registration with multiple workers 1`] = `
+<li
+ className=""
+>
+ <article
+ className="registration js-sw-container"
+ >
+ <header
+ className="registration__header"
+ >
+ <h2
+ className="registration__scope js-sw-scope devtools-ellipsis-text"
+ title="SCOPE 123"
+ >
+ SCOPE 123
+ </h2>
+ </header>
+ <aside
+ className="registration__controls"
+ >
+ <Localized
+ id="serviceworker-worker-unregister"
+ >
+ <UIButton
+ className="js-unregister-button"
+ onClick={[Function]}
+ />
+ </Localized>
+ </aside>
+ <ul
+ className="registration__workers"
+ >
+ <li
+ className="registration__workers-item"
+ key="id-worker-1-example"
+ >
+ <Connect(Worker)
+ isDebugEnabled={true}
+ worker={
+ Object {
+ "id": "id-worker-1-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ }
+ }
+ />
+ </li>
+ <li
+ className="registration__workers-item"
+ key="id-worker-2-example"
+ >
+ <Connect(Worker)
+ isDebugEnabled={true}
+ worker={
+ Object {
+ "id": "id-worker-2-example",
+ "state": 2,
+ "stateText": "installed",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ }
+ }
+ />
+ </li>
+ </ul>
+ </article>
+</li>
+`;
+
+exports[`Registration Renders the expected snapshot when sw debugging is disabled 1`] = `
+<li
+ className=""
+>
+ <article
+ className="registration js-sw-container"
+ >
+ <header
+ className="registration__header"
+ >
+ <h2
+ className="registration__scope js-sw-scope devtools-ellipsis-text"
+ title="SCOPE 123"
+ >
+ SCOPE 123
+ </h2>
+ </header>
+ <aside
+ className="registration__controls"
+ >
+ <Localized
+ id="serviceworker-worker-unregister"
+ >
+ <UIButton
+ className="js-unregister-button"
+ onClick={[Function]}
+ />
+ </Localized>
+ </aside>
+ <ul
+ className="registration__workers"
+ >
+ <li
+ className="registration__workers-item"
+ key="id-worker-1-example"
+ >
+ <Connect(Worker)
+ isDebugEnabled={false}
+ worker={
+ Object {
+ "id": "id-worker-1-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ }
+ }
+ />
+ </li>
+ </ul>
+ </article>
+</li>
+`;
diff --git a/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationList.test.js.snap b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationList.test.js.snap
new file mode 100644
index 0000000000..160bff7c8c
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationList.test.js.snap
@@ -0,0 +1,159 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RegistrationList renders the expected snapshot for a list with a single registration 1`] = `
+Array [
+ <article
+ className="registrations-container"
+ key="registrations-container"
+ >
+ <Localized
+ id="serviceworker-list-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ul
+ className="registrations-container__list"
+ >
+ <Connect(Registration)
+ className="registrations-container__item"
+ isDebugEnabled={true}
+ key="id-reg-1-example"
+ registration={
+ Object {
+ "id": "id-reg-1-example",
+ "registrationFront": "",
+ "scope": "SCOPE 123",
+ "workers": Array [
+ Object {
+ "id": "id-worker-1-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ }
+ }
+ />
+ </ul>
+ </article>,
+ <footer
+ className="aboutdebugging-plug"
+ >
+ <Localized
+ a={
+ <a
+ className="aboutdebugging-plug__link"
+ onClick={[Function]}
+ />
+ }
+ id="serviceworker-list-aboutdebugging"
+ key="serviceworkerlist-footer"
+ >
+ <p />
+ </Localized>
+ </footer>,
+]
+`;
+
+exports[`RegistrationList renders the expected snapshot for a multiple registration list 1`] = `
+Array [
+ <article
+ className="registrations-container"
+ key="registrations-container"
+ >
+ <Localized
+ id="serviceworker-list-header"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <ul
+ className="registrations-container__list"
+ >
+ <Connect(Registration)
+ className="registrations-container__item"
+ isDebugEnabled={true}
+ key="id-reg-1-example"
+ registration={
+ Object {
+ "id": "id-reg-1-example",
+ "registrationFront": "",
+ "scope": "SCOPE1",
+ "workers": Array [
+ Object {
+ "id": "id-worker-1-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ }
+ }
+ />
+ <Connect(Registration)
+ className="registrations-container__item"
+ isDebugEnabled={true}
+ key="id-reg-1-example"
+ registration={
+ Object {
+ "id": "id-reg-1-example",
+ "registrationFront": "",
+ "scope": "SCOPE2",
+ "workers": Array [
+ Object {
+ "id": "id-worker-2-example",
+ "state": 2,
+ "stateText": "installed",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ }
+ }
+ />
+ <Connect(Registration)
+ className="registrations-container__item"
+ isDebugEnabled={true}
+ key="id-reg-3-example"
+ registration={
+ Object {
+ "id": "id-reg-3-example",
+ "registrationFront": "",
+ "scope": "SCOPE3",
+ "workers": Array [
+ Object {
+ "id": "id-worker-3-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ }
+ }
+ />
+ </ul>
+ </article>,
+ <footer
+ className="aboutdebugging-plug"
+ >
+ <Localized
+ a={
+ <a
+ className="aboutdebugging-plug__link"
+ onClick={[Function]}
+ />
+ }
+ id="serviceworker-list-aboutdebugging"
+ key="serviceworkerlist-footer"
+ >
+ <p />
+ </Localized>
+ </footer>,
+]
+`;
diff --git a/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationListEmpty.test.js.snap b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationListEmpty.test.js.snap
new file mode 100644
index 0000000000..657c7164d7
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationListEmpty.test.js.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RegistrationListEmpty renders the expected snapshot 1`] = `
+<article
+ className="app-page__icon-container js-registration-list-empty"
+>
+ <aside>
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ }
+ }
+ id="sidebar-item-service-workers"
+ >
+ <img
+ className="app-page__icon"
+ src="chrome://devtools/skin/images/debugging-workers.svg"
+ />
+ </Localized>
+ </aside>
+ <div>
+ <Localized
+ id="serviceworker-empty-intro2"
+ >
+ <h1
+ className="app-page__title"
+ />
+ </Localized>
+ <Localized
+ a={
+ <a
+ onClick={[Function]}
+ />
+ }
+ id="serviceworker-empty-suggestions2"
+ span={
+ <a
+ onClick={[Function]}
+ />
+ }
+ >
+ <p />
+ </Localized>
+ <p>
+ <Localized
+ id="serviceworker-empty-intro-link"
+ >
+ <a
+ onClick={[Function]}
+ />
+ </Localized>
+ </p>
+ <p>
+ <Localized
+ id="serviceworker-empty-suggestions-aboutdebugging2"
+ >
+ <a
+ className="js-trusted-link"
+ onClick={[Function]}
+ />
+ </Localized>
+ </p>
+ </div>
+</article>
+`;
diff --git a/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-Worker.test.js.snap b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-Worker.test.js.snap
new file mode 100644
index 0000000000..7e71765e90
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-Worker.test.js.snap
@@ -0,0 +1,132 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Worker Renders the expected snapshot for a non-active worker 1`] = `
+<section
+ className="worker js-sw-worker"
+>
+ <p
+ className="worker__icon"
+ >
+ <img
+ className="worker__icon-image"
+ src="chrome://devtools/skin/images/debugging-workers.svg"
+ />
+ </p>
+ <p
+ className="worker__source"
+ >
+ <span
+ className="js-source-url"
+ >
+ worker.js
+ </span>
+ </p>
+ <p
+ className="worker__misc"
+ >
+ <span
+ className="js-worker-status worker__status worker__status--waiting"
+ >
+ installed
+ </span>
+
+ </p>
+</section>
+`;
+
+exports[`Worker Renders the expected snapshot for a running worker 1`] = `
+<section
+ className="worker js-sw-worker"
+>
+ <p
+ className="worker__icon"
+ >
+ <img
+ className="worker__icon-image"
+ src="chrome://devtools/skin/images/debugging-workers.svg"
+ />
+ </p>
+ <p
+ className="worker__source"
+ >
+ <a
+ className="js-inspect-link"
+ href="#"
+ onClick={[Function]}
+ title="http://example.com/worker.js"
+ >
+ <span
+ className="js-source-url"
+ >
+ worker.js
+ </span>
+  
+ <Localized
+ attrs={
+ Object {
+ "alt": true,
+ }
+ }
+ id="serviceworker-worker-inspect-icon"
+ >
+ <img
+ src="chrome://devtools/skin/images/application-debug.svg"
+ />
+ </Localized>
+ </a>
+ </p>
+ <p
+ className="worker__misc"
+ >
+ <span
+ className="js-worker-status worker__status worker__status--active"
+ >
+ serviceworker-worker-status-running
+ </span>
+
+ </p>
+</section>
+`;
+
+exports[`Worker Renders the expected snapshot for a stopped worker 1`] = `
+<section
+ className="worker js-sw-worker"
+>
+ <p
+ className="worker__icon"
+ >
+ <img
+ className="worker__icon-image"
+ src="chrome://devtools/skin/images/debugging-workers.svg"
+ />
+ </p>
+ <p
+ className="worker__source"
+ >
+ <span
+ className="js-source-url"
+ >
+ worker.js
+ </span>
+ </p>
+ <p
+ className="worker__misc"
+ >
+ <span
+ className="js-worker-status worker__status worker__status--active"
+ >
+ serviceworker-worker-status-stopped
+ </span>
+
+ <Localized
+ id="serviceworker-worker-start3"
+ >
+ <UIButton
+ className="js-start-button"
+ onClick={[Function]}
+ size="micro"
+ />
+ </Localized>
+ </p>
+</section>
+`;
diff --git a/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-WorkersPage.test.js.snap b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-WorkersPage.test.js.snap
new file mode 100644
index 0000000000..5de3e33b4e
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-WorkersPage.test.js.snap
@@ -0,0 +1,143 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WorkersPage filters out workers from diferent domains 1`] = `
+<section
+ className="app-page js-service-workers-page "
+>
+ <RegistrationList
+ canDebugWorkers={true}
+ registrations={
+ Array [
+ Object {
+ "id": "id-reg-1-example",
+ "registrationFront": "",
+ "scope": "SCOPE1",
+ "workers": Array [
+ Object {
+ "id": "id-worker-1-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ },
+ Object {
+ "id": "id-reg-2-example",
+ "registrationFront": "",
+ "scope": "SCOPE2",
+ "workers": Array [
+ Object {
+ "id": "id-worker-2-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ },
+ ]
+ }
+ />
+</section>
+`;
+
+exports[`WorkersPage filters out workers from different domains and renders an empty list when there is none left 1`] = `
+<section
+ className="app-page js-service-workers-page app-page--empty"
+>
+ <RegistrationListEmpty />
+</section>
+`;
+
+exports[`WorkersPage it renders a list with a single element if there's just 1 worker 1`] = `
+<section
+ className="app-page js-service-workers-page "
+>
+ <RegistrationList
+ canDebugWorkers={true}
+ registrations={
+ Array [
+ Object {
+ "id": "id-reg-1-example",
+ "registrationFront": "",
+ "scope": "SCOPE 123",
+ "workers": Array [
+ Object {
+ "id": "id-worker-1-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ },
+ ]
+ }
+ />
+</section>
+`;
+
+exports[`WorkersPage renders a list with multiple elements when there are multiple workers 1`] = `
+<section
+ className="app-page js-service-workers-page "
+>
+ <RegistrationList
+ canDebugWorkers={true}
+ registrations={
+ Array [
+ Object {
+ "id": "id-reg-1-example",
+ "registrationFront": "",
+ "scope": "SCOPE1",
+ "workers": Array [
+ Object {
+ "id": "id-worker-1-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ },
+ Object {
+ "id": "id-reg-1-example",
+ "registrationFront": "",
+ "scope": "SCOPE2",
+ "workers": Array [
+ Object {
+ "id": "id-worker-2-example",
+ "state": 2,
+ "stateText": "installed",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ },
+ Object {
+ "id": "id-reg-3-example",
+ "registrationFront": "",
+ "scope": "SCOPE3",
+ "workers": Array [
+ Object {
+ "id": "id-worker-3-example",
+ "state": 4,
+ "stateText": "activated",
+ "url": "http://example.com/worker.js",
+ "workerDescriptorFront": "",
+ },
+ ],
+ },
+ ]
+ }
+ />
+</section>
+`;
+
+exports[`WorkersPage renders an empty list if there are no workers 1`] = `
+<section
+ className="app-page js-service-workers-page app-page--empty"
+>
+ <RegistrationListEmpty />
+</section>
+`;
diff --git a/devtools/client/application/test/node/components/service-workers/components_application_panel-Registration.test.js b/devtools/client/application/test/node/components/service-workers/components_application_panel-Registration.test.js
new file mode 100644
index 0000000000..7473a61d23
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/components_application_panel-Registration.test.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+// Import test helpers
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+
+const {
+ REGISTRATION_SINGLE_WORKER,
+ REGISTRATION_MULTIPLE_WORKERS,
+} = require("resource://devtools/client/application/test/node/fixtures/data/constants.js");
+
+const Registration = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/Registration.js")
+);
+
+describe("Registration", () => {
+ it("Renders the expected snapshot for a registration with a worker", () => {
+ const store = setupStore({});
+
+ const wrapper = shallow(
+ Registration({
+ isDebugEnabled: true,
+ registration: REGISTRATION_SINGLE_WORKER,
+ store,
+ })
+ ).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ // ensure that we do have the proper amount of workers
+ expect(wrapper.find("Connect(Worker)")).toHaveLength(1);
+ });
+
+ it("Renders the expected snapshot for a registration with multiple workers", () => {
+ const store = setupStore({});
+
+ const wrapper = shallow(
+ Registration({
+ isDebugEnabled: true,
+ registration: REGISTRATION_MULTIPLE_WORKERS,
+ store,
+ })
+ ).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ // ensure that we do have the proper amount of workers
+ expect(wrapper.find("Connect(Worker)")).toHaveLength(2);
+ });
+
+ it("Renders the expected snapshot when sw debugging is disabled", () => {
+ const store = setupStore({});
+
+ const wrapper = shallow(
+ Registration({
+ isDebugEnabled: false,
+ registration: REGISTRATION_SINGLE_WORKER,
+ store,
+ })
+ ).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("Removes the ending forward slash from the scope, when present", () => {
+ const store = setupStore({});
+
+ const registration = Object.assign({}, REGISTRATION_SINGLE_WORKER, {
+ scope: "https://example.com/something/",
+ });
+
+ const wrapper = shallow(
+ Registration({
+ isDebugEnabled: false,
+ registration,
+ store,
+ })
+ ).dive();
+
+ const scopeEl = wrapper.find(".js-sw-scope");
+ expect(scopeEl.text()).toBe("example.com/something");
+ });
+});
diff --git a/devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationList.test.js b/devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationList.test.js
new file mode 100644
index 0000000000..6bb202cd25
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationList.test.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+// Import constants
+const {
+ SINGLE_WORKER_DEFAULT_DOMAIN_LIST,
+ MULTIPLE_WORKER_LIST,
+} = require("resource://devtools/client/application/test/node/fixtures/data/constants.js");
+
+const RegistrationList = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/RegistrationList.js")
+);
+
+/**
+ * Test for RegistrationList.js component
+ */
+describe("RegistrationList", () => {
+ it("renders the expected snapshot for a list with a single registration", () => {
+ const wrapper = shallow(
+ RegistrationList({
+ registrations: SINGLE_WORKER_DEFAULT_DOMAIN_LIST,
+ canDebugWorkers: true,
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders the expected snapshot for a multiple registration list", () => {
+ const wrapper = shallow(
+ RegistrationList({
+ registrations: MULTIPLE_WORKER_LIST,
+ canDebugWorkers: true,
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationListEmpty.test.js b/devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationListEmpty.test.js
new file mode 100644
index 0000000000..811535facc
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationListEmpty.test.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+const RegistrationListEmpty = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/RegistrationListEmpty.js")
+);
+
+/**
+ * Test for RegistrationListEmpty.js component
+ */
+
+describe("RegistrationListEmpty", () => {
+ it("renders the expected snapshot", () => {
+ const wrapper = shallow(RegistrationListEmpty({}));
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/service-workers/components_application_panel-Worker.test.js b/devtools/client/application/test/node/components/service-workers/components_application_panel-Worker.test.js
new file mode 100644
index 0000000000..36a6acda0c
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/components_application_panel-Worker.test.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+// Import test helpers
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+
+const {
+ WORKER_RUNNING,
+ WORKER_STOPPED,
+ WORKER_WAITING,
+} = require("resource://devtools/client/application/test/node/fixtures/data/constants.js");
+
+const Worker = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/Worker.js")
+);
+
+describe("Worker", () => {
+ it("Renders the expected snapshot for a running worker", () => {
+ const store = setupStore({});
+
+ const wrapper = shallow(
+ Worker({
+ isDebugEnabled: true,
+ worker: WORKER_RUNNING,
+ store,
+ })
+ ).dive();
+
+ // ensure proper status
+ expect(wrapper.find(".js-worker-status").text()).toBe(
+ "serviceworker-worker-status-running"
+ );
+ // check that Start button is not available
+ expect(wrapper.find(".js-start-button")).toHaveLength(0);
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("Renders the expected snapshot for a stopped worker", () => {
+ const store = setupStore({});
+
+ const wrapper = shallow(
+ Worker({
+ isDebugEnabled: true,
+ worker: WORKER_STOPPED,
+ store,
+ })
+ ).dive();
+
+ // ensure proper status
+ expect(wrapper.find(".js-worker-status").text()).toBe(
+ "serviceworker-worker-status-stopped"
+ );
+ // check that Start button is available
+ expect(wrapper.find(".js-start-button")).toHaveLength(1);
+ // check that inspect link does not exist
+ expect(wrapper.find(".js-inspect-link")).toHaveLength(0);
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("Renders the start button even if debugging workers is disabled", () => {
+ const store = setupStore({});
+
+ const wrapper = shallow(
+ Worker({
+ isDebugEnabled: false,
+ worker: WORKER_STOPPED,
+ store,
+ })
+ ).dive();
+
+ // ensure proper status
+ expect(wrapper.find(".js-worker-status").text()).toBe(
+ "serviceworker-worker-status-stopped"
+ );
+ // check that Start button is available
+ expect(wrapper.find(".js-start-button")).toHaveLength(1);
+ });
+
+ it("Renders the expected snapshot for a non-active worker", () => {
+ const store = setupStore({});
+
+ const wrapper = shallow(
+ Worker({
+ isDebugEnabled: true,
+ worker: WORKER_WAITING,
+ store,
+ })
+ ).dive();
+
+ // ensure proper status
+ // NOTE: since non-active status are localized directly in the front, not
+ // in the panel, we don't expect a localization ID here
+ expect(wrapper.find(".js-worker-status").text()).toBe("installed");
+ // check that Start button is not available
+ expect(wrapper.find(".js-start-button")).toHaveLength(0);
+ // check that Debug link does not exist
+ expect(wrapper.find(".js-inspect-link")).toHaveLength(0);
+
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/application/test/node/components/service-workers/components_application_panel-WorkersPage.test.js b/devtools/client/application/test/node/components/service-workers/components_application_panel-WorkersPage.test.js
new file mode 100644
index 0000000000..be1b14f216
--- /dev/null
+++ b/devtools/client/application/test/node/components/service-workers/components_application_panel-WorkersPage.test.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Import libs
+const { shallow } = require("enzyme");
+const { createFactory } = require("react");
+
+// Import fixtures
+const {
+ EMPTY_WORKER_LIST,
+ SINGLE_WORKER_DEFAULT_DOMAIN_LIST,
+ SINGLE_WORKER_DIFFERENT_DOMAIN_LIST,
+ MULTIPLE_WORKER_LIST,
+ MULTIPLE_WORKER_MIXED_DOMAINS_LIST,
+} = require("resource://devtools/client/application/test/node/fixtures/data/constants.js");
+
+// Import setupStore with imported & combined reducers
+const {
+ setupStore,
+} = require("resource://devtools/client/application/test/node/helpers.js");
+
+// Import component
+const WorkersPage = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/WorkersPage.js")
+);
+
+/**
+ * Test for App.js component
+ */
+describe("WorkersPage", () => {
+ const baseState = {
+ workers: { list: [], canDebugWorkers: true },
+ page: { domain: "example.com" },
+ };
+
+ function buildStoreWithWorkers(workerList) {
+ const workers = { list: workerList, canDebugWorkers: true };
+ const state = Object.assign({}, baseState, { workers });
+ return setupStore(state);
+ }
+
+ it("renders an empty list if there are no workers", () => {
+ const store = buildStoreWithWorkers(EMPTY_WORKER_LIST);
+ const wrapper = shallow(WorkersPage({ store })).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("it renders a list with a single element if there's just 1 worker", () => {
+ const store = buildStoreWithWorkers(SINGLE_WORKER_DEFAULT_DOMAIN_LIST);
+ const wrapper = shallow(WorkersPage({ store })).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders a list with multiple elements when there are multiple workers", () => {
+ const store = buildStoreWithWorkers(MULTIPLE_WORKER_LIST);
+ const wrapper = shallow(WorkersPage({ store })).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("filters out workers from diferent domains", () => {
+ const store = buildStoreWithWorkers(MULTIPLE_WORKER_MIXED_DOMAINS_LIST);
+ const wrapper = shallow(WorkersPage({ store })).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it(
+ "filters out workers from different domains and renders an empty list when " +
+ "there is none left",
+ () => {
+ const store = buildStoreWithWorkers(SINGLE_WORKER_DIFFERENT_DOMAIN_LIST);
+ const wrapper = shallow(WorkersPage({ store })).dive();
+
+ expect(wrapper).toMatchSnapshot();
+ }
+ );
+});
diff --git a/devtools/client/application/test/node/fixtures/data/constants.js b/devtools/client/application/test/node/fixtures/data/constants.js
new file mode 100644
index 0000000000..795324525c
--- /dev/null
+++ b/devtools/client/application/test/node/fixtures/data/constants.js
@@ -0,0 +1,312 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// NOTE: worker state values are defined in an enum in nsIServiceWorkerManager
+// https://searchfox.org/mozilla-central/source/dom/interfaces/base/nsIServiceWorkerManager.idl
+
+const EMPTY_WORKER_LIST = [];
+
+const WORKER_RUNNING = {
+ id: "id-worker-1-example",
+ workerDescriptorFront: true,
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+};
+
+const WORKER_STOPPED = {
+ id: "id-worker-1-example",
+ workerDescriptorFront: false,
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+};
+
+const WORKER_WAITING = {
+ id: "id-worker-1-example",
+ workerDescriptorFront: false,
+ url: "http://example.com/worker.js",
+ state: 2,
+ stateText: "installed",
+};
+
+const REGISTRATION_SINGLE_WORKER = {
+ id: "id-reg-1-example",
+ scope: "SCOPE 123",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-1-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ ],
+};
+
+const REGISTRATION_MULTIPLE_WORKERS = {
+ id: "id-reg-1-example",
+ scope: "SCOPE 123",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-1-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ {
+ id: "id-worker-2-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 2,
+ stateText: "installed",
+ },
+ ],
+};
+
+const SINGLE_WORKER_DEFAULT_DOMAIN_LIST = [
+ {
+ id: "id-reg-1-example",
+ scope: "SCOPE 123",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-1-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ ],
+ },
+];
+
+const SINGLE_WORKER_DIFFERENT_DOMAIN_LIST = [
+ {
+ id: "id-reg-1-example",
+ scope: "SCOPE 123",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-1-example",
+ workerDescriptorFront: "",
+ url: "http://different-example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ ],
+ },
+];
+
+const MULTIPLE_WORKER_LIST = [
+ {
+ id: "id-reg-1-example",
+ scope: "SCOPE1",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-1-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ ],
+ },
+ {
+ id: "id-reg-1-example",
+ scope: "SCOPE2",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-2-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 2,
+ stateText: "installed",
+ },
+ ],
+ },
+ {
+ id: "id-reg-3-example",
+ scope: "SCOPE3",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-3-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ ],
+ },
+];
+
+const MULTIPLE_WORKER_MIXED_DOMAINS_LIST = [
+ {
+ id: "id-reg-1-example",
+ scope: "SCOPE1",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-1-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ ],
+ },
+ {
+ id: "id-reg-2-example",
+ scope: "SCOPE2",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-2-example",
+ workerDescriptorFront: "",
+ url: "http://example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ ],
+ },
+ {
+ id: "id-reg-3-example",
+ scope: "SCOPE3",
+ registrationFront: "",
+ workers: [
+ {
+ id: "id-worker-3-example",
+ workerDescriptorFront: "",
+ url: "http://different-example.com/worker.js",
+ state: 4,
+ stateText: "activated",
+ },
+ ],
+ },
+];
+
+// props for a simple manifest
+const MANIFEST_SIMPLE = {
+ icons: [
+ {
+ key: { sizes: "1x1", contentType: "image/png" },
+ value: { src: "something.png", purpose: "any" },
+ type: "icon",
+ },
+ ],
+ identity: [{ key: "name", value: "foo", type: "string" }],
+ presentation: [
+ { key: "lorem", value: "ipsum", type: "string" },
+ { key: "foo", value: "bar", type: "string" },
+ ],
+ validation: [{ level: "warning", message: "This is a warning" }],
+};
+
+// props for a manifest with string values
+const MANIFEST_STRING_MEMBERS = {
+ icons: [],
+ identity: [{ key: "name", value: "foo", type: "string" }],
+ presentation: [],
+ validation: [],
+};
+
+// props for a manifest with color values
+const MANIFEST_COLOR_MEMBERS = {
+ icons: [],
+ identity: [],
+ presentation: [
+ { key: "background_color", value: "red", type: "color" },
+ { key: "theme_color", value: "rgb(0, 0, 0)", type: "color" },
+ ],
+ validation: [],
+};
+
+// props for a manifest with icon values
+const MANIFEST_ICON_MEMBERS = {
+ icons: [
+ {
+ key: { sizes: "1x1", contentType: "image/png" },
+ value: { src: "something.png", purpose: "any" },
+ type: "icon",
+ },
+ {
+ key: { sizes: "", contentType: "" },
+ value: { src: "something.svg", purpose: "any maskable" },
+ type: "icon",
+ },
+ ],
+ identity: [],
+ presentation: [],
+ validation: [],
+};
+
+// props for a manifest with values that have an unrecognized type
+const MANIFEST_UNKNOWN_TYPE_MEMBERS = {
+ icons: [],
+ identity: [{ key: "lorem", value: "ipsum", type: "foo" }],
+ presentation: [],
+ validation: [],
+};
+
+// props for a manifest with url values
+const MANIFEST_URL_MEMBERS = {
+ icons: [],
+ identity: [],
+ presentation: [
+ { key: "start_url", value: "https://example.com/", type: "url" },
+ { key: "scope", value: "https://example.com/", type: "url" },
+ ],
+};
+
+const MANIFEST_WITH_ISSUES = {
+ icons: [],
+ identity: [{ key: "name", value: "foo", type: "string" }],
+ presentation: [
+ { key: "lorem", value: "ipsum", type: "string" },
+ { key: "foo", value: "bar", type: "string" },
+ ],
+ validation: [{ level: "warning", message: "This is a warning" }],
+};
+
+// props for a manifest with no validation issues
+const MANIFEST_NO_ISSUES = {
+ icons: [],
+ identity: [{ key: "name", value: "foo", type: "string" }],
+ presentation: [
+ { key: "lorem", value: "ipsum", type: "string" },
+ { key: "foo", value: "bar", type: "string" },
+ ],
+ validation: [],
+};
+
+module.exports = {
+ // service worker related fixtures
+ EMPTY_WORKER_LIST,
+ MULTIPLE_WORKER_LIST,
+ MULTIPLE_WORKER_MIXED_DOMAINS_LIST,
+ REGISTRATION_MULTIPLE_WORKERS,
+ REGISTRATION_SINGLE_WORKER,
+ SINGLE_WORKER_DEFAULT_DOMAIN_LIST,
+ SINGLE_WORKER_DIFFERENT_DOMAIN_LIST,
+ WORKER_RUNNING,
+ WORKER_STOPPED,
+ WORKER_WAITING,
+ // manifest related fixtures
+ MANIFEST_NO_ISSUES,
+ MANIFEST_WITH_ISSUES,
+ MANIFEST_SIMPLE,
+ MANIFEST_COLOR_MEMBERS,
+ MANIFEST_ICON_MEMBERS,
+ MANIFEST_STRING_MEMBERS,
+ MANIFEST_UNKNOWN_TYPE_MEMBERS,
+ MANIFEST_URL_MEMBERS,
+};
diff --git a/devtools/client/application/test/node/helpers.js b/devtools/client/application/test/node/helpers.js
new file mode 100644
index 0000000000..6699549a15
--- /dev/null
+++ b/devtools/client/application/test/node/helpers.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ thunk,
+} = require("resource://devtools/client/shared/redux/middleware/thunk.js");
+const configureStore = require("redux-mock-store").default;
+
+/**
+ * Prepare the store for use in testing.
+ */
+function setupStore(preloadedState = {}) {
+ const middleware = [thunk()];
+ const mockStore = configureStore(middleware);
+ return mockStore(preloadedState);
+}
+
+/**
+ * This gives an opportunity to Promises to resolve in tests
+ * (since they are microtasks)
+ */
+async function flushPromises() {
+ await new Promise(r => setTimeout(r, 0));
+}
+
+module.exports = {
+ flushPromises,
+ setupStore,
+};
diff --git a/devtools/client/application/test/node/jest.config.js b/devtools/client/application/test/node/jest.config.js
new file mode 100644
index 0000000000..e114658f88
--- /dev/null
+++ b/devtools/client/application/test/node/jest.config.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global __dirname */
+
+const sharedJestConfig = require(`${__dirname}/../../../shared/test-helpers/shared-jest.config`);
+
+module.exports = {
+ ...sharedJestConfig,
+ setupFiles: ["<rootDir>setup.js"],
+ snapshotSerializers: ["enzyme-to-json/serializer"],
+};
diff --git a/devtools/client/application/test/node/package.json b/devtools/client/application/test/node/package.json
new file mode 100644
index 0000000000..3bfea24f40
--- /dev/null
+++ b/devtools/client/application/test/node/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "application-panel-tests",
+ "license": "MPL-2.0",
+ "version": "0.0.1",
+ "engines": {
+ "node": ">=8.9.4"
+ },
+ "scripts": {
+ "test": "jest",
+ "test-ci": "jest --json"
+ },
+ "dependencies": {
+ "@babel/plugin-proposal-async-generator-functions": "^7.2.0",
+ "enzyme": "^3.9.0",
+ "enzyme-adapter-react-16": "^1.13.2",
+ "enzyme-to-json": "^3.3.5",
+ "jest": "^24.6",
+ "react": "16.4.1",
+ "react-dom": "16",
+ "react-test-renderer": "16.4.1",
+ "redux": "^4.0.4",
+ "redux-mock-store": "^1.5.3"
+ },
+ "devDependencies": {}
+}
diff --git a/devtools/client/application/test/node/setup.js b/devtools/client/application/test/node/setup.js
new file mode 100644
index 0000000000..570e4462ae
--- /dev/null
+++ b/devtools/client/application/test/node/setup.js
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"use strict";
+
+// Configure enzyme with React 16 adapter.
+const Enzyme = require("enzyme");
+const Adapter = require("enzyme-adapter-react-16");
+Enzyme.configure({ adapter: new Adapter() });
+
+const {
+ setMocksInGlobal,
+} = require("resource://devtools/client/shared/test-helpers/shared-node-helpers.js");
+setMocksInGlobal();
diff --git a/devtools/client/application/test/node/yarn.lock b/devtools/client/application/test/node/yarn.lock
new file mode 100644
index 0000000000..91f1c4660c
--- /dev/null
+++ b/devtools/client/application/test/node/yarn.lock
@@ -0,0 +1,3563 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
+ dependencies:
+ "@babel/highlight" "^7.0.0"
+
+"@babel/core@^7.1.0":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.4.4"
+ "@babel/helpers" "^7.4.4"
+ "@babel/parser" "^7.4.5"
+ "@babel/template" "^7.4.4"
+ "@babel/traverse" "^7.4.5"
+ "@babel/types" "^7.4.4"
+ convert-source-map "^1.1.0"
+ debug "^4.1.0"
+ json5 "^2.1.0"
+ lodash "^4.17.11"
+ resolve "^1.3.2"
+ semver "^5.4.1"
+ source-map "^0.5.0"
+
+"@babel/generator@^7.4.0", "@babel/generator@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041"
+ dependencies:
+ "@babel/types" "^7.4.4"
+ jsesc "^2.5.1"
+ lodash "^4.17.11"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+"@babel/helper-annotate-as-pure@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32"
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-function-name@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.0.0"
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-get-function-arity@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3"
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-plugin-utils@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
+
+"@babel/helper-remap-async-to-generator@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f"
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-wrap-function" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-split-export-declaration@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677"
+ dependencies:
+ "@babel/types" "^7.4.4"
+
+"@babel/helper-wrap-function@^7.1.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa"
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.2.0"
+
+"@babel/helpers@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5"
+ dependencies:
+ "@babel/template" "^7.4.4"
+ "@babel/traverse" "^7.4.4"
+ "@babel/types" "^7.4.4"
+
+"@babel/highlight@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872"
+
+"@babel/plugin-proposal-async-generator-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-remap-async-to-generator" "^7.1.0"
+ "@babel/plugin-syntax-async-generators" "^7.2.0"
+
+"@babel/plugin-syntax-async-generators@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-object-rest-spread@^7.0.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e"
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/parser" "^7.4.4"
+ "@babel/types" "^7.4.4"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216"
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.4.4"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.4.4"
+ "@babel/parser" "^7.4.5"
+ "@babel/types" "^7.4.4"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.11"
+
+"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0"
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.11"
+ to-fast-properties "^2.0.0"
+
+"@cnakazawa/watch@^1.0.3":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
+ dependencies:
+ exec-sh "^0.3.2"
+ minimist "^1.2.0"
+
+"@jest/console@^24.7.1":
+ version "24.7.1"
+ resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545"
+ dependencies:
+ "@jest/source-map" "^24.3.0"
+ chalk "^2.0.1"
+ slash "^2.0.0"
+
+"@jest/core@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.8.0.tgz#fbbdcd42a41d0d39cddbc9f520c8bab0c33eed5b"
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/reporters" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-changed-files "^24.8.0"
+ jest-config "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve-dependencies "^24.8.0"
+ jest-runner "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ jest-watcher "^24.8.0"
+ micromatch "^3.1.10"
+ p-each-series "^1.0.0"
+ pirates "^4.0.1"
+ realpath-native "^1.1.0"
+ rimraf "^2.5.4"
+ strip-ansi "^5.0.0"
+
+"@jest/environment@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.8.0.tgz#0342261383c776bdd652168f68065ef144af0eac"
+ dependencies:
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+
+"@jest/fake-timers@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.8.0.tgz#2e5b80a4f78f284bcb4bd5714b8e10dd36a8d3d1"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-mock "^24.8.0"
+
+"@jest/reporters@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.8.0.tgz#075169cd029bddec54b8f2c0fc489fd0b9e05729"
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.2"
+ istanbul-lib-coverage "^2.0.2"
+ istanbul-lib-instrument "^3.0.1"
+ istanbul-lib-report "^2.0.4"
+ istanbul-lib-source-maps "^3.0.1"
+ istanbul-reports "^2.1.1"
+ jest-haste-map "^24.8.0"
+ jest-resolve "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ node-notifier "^5.2.1"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+ string-length "^2.0.0"
+
+"@jest/source-map@^24.3.0":
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.3.0.tgz#563be3aa4d224caf65ff77edc95cd1ca4da67f28"
+ dependencies:
+ callsites "^3.0.0"
+ graceful-fs "^4.1.15"
+ source-map "^0.6.0"
+
+"@jest/test-result@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.8.0.tgz#7675d0aaf9d2484caa65e048d9b467d160f8e9d3"
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/types" "^24.8.0"
+ "@types/istanbul-lib-coverage" "^2.0.0"
+
+"@jest/test-sequencer@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz#2f993bcf6ef5eb4e65e8233a95a3320248cf994b"
+ dependencies:
+ "@jest/test-result" "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-runner "^24.8.0"
+ jest-runtime "^24.8.0"
+
+"@jest/transform@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.8.0.tgz#628fb99dce4f9d254c6fd9341e3eea262e06fef5"
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/types" "^24.8.0"
+ babel-plugin-istanbul "^5.1.0"
+ chalk "^2.0.1"
+ convert-source-map "^1.4.0"
+ fast-json-stable-stringify "^2.0.0"
+ graceful-fs "^4.1.15"
+ jest-haste-map "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-util "^24.8.0"
+ micromatch "^3.1.10"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ source-map "^0.6.1"
+ write-file-atomic "2.4.1"
+
+"@jest/types@^24.8.0":
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.8.0.tgz#f31e25948c58f0abd8c845ae26fcea1491dea7ad"
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^1.1.1"
+ "@types/yargs" "^12.0.9"
+
+"@types/babel__core@^7.1.0":
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f"
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc"
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307"
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
+ version "7.0.7"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f"
+ dependencies:
+ "@babel/types" "^7.3.0"
+
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
+
+"@types/istanbul-lib-report@*":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c"
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a"
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+ "@types/istanbul-lib-report" "*"
+
+"@types/node@*":
+ version "12.0.8"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.8.tgz#551466be11b2adc3f3d47156758f610bd9f6b1d8"
+
+"@types/stack-utils@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
+
+"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
+ version "12.0.12"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
+
+abab@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+
+acorn-globals@^4.1.0:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.2.tgz#4e2c2313a597fd589720395f6354b41cd5ec8006"
+ dependencies:
+ acorn "^6.0.1"
+ acorn-walk "^6.0.1"
+
+acorn-walk@^6.0.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
+
+acorn@^5.5.3:
+ version "5.7.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
+
+acorn@^6.0.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
+
+airbnb-prop-types@^2.13.2:
+ version "2.13.2"
+ resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz#43147a5062dd2a4a5600e748a47b64004cc5f7fc"
+ dependencies:
+ array.prototype.find "^2.0.4"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
+ is-regex "^1.0.4"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.1.0"
+ prop-types "^15.7.2"
+ prop-types-exact "^1.2.0"
+ react-is "^16.8.6"
+
+ajv@^6.5.5:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-escapes@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-regex@^4.0.0, ansi-regex@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+
+array-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+
+array.prototype.find@^2.0.4:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7"
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.13.0"
+
+array.prototype.flat@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.10.0"
+ function-bind "^1.1.1"
+
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+
+async-limiter@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+atob@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+
+babel-jest@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.8.0.tgz#5c15ff2b28e20b0f45df43fe6b7f2aae93dba589"
+ dependencies:
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/babel__core" "^7.1.0"
+ babel-plugin-istanbul "^5.1.0"
+ babel-preset-jest "^24.6.0"
+ chalk "^2.4.2"
+ slash "^2.0.0"
+
+babel-plugin-istanbul@^5.1.0:
+ version "5.1.4"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz#841d16b9a58eeb407a0ddce622ba02fe87a752ba"
+ dependencies:
+ find-up "^3.0.0"
+ istanbul-lib-instrument "^3.3.0"
+ test-exclude "^5.2.3"
+
+babel-plugin-jest-hoist@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz#f7f7f7ad150ee96d7a5e8e2c5da8319579e78019"
+ dependencies:
+ "@types/babel__traverse" "^7.0.6"
+
+babel-preset-jest@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz#66f06136eefce87797539c0d63f1769cc3915984"
+ dependencies:
+ "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
+ babel-plugin-jest-hoist "^24.6.0"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+browser-process-hrtime@^0.1.2:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4"
+
+browser-resolve@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+ dependencies:
+ resolve "1.1.7"
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+
+camelcase@^5.0.0:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+
+capture-exit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
+ dependencies:
+ rsvp "^4.8.4"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+cheerio@^1.0.0-rc.2:
+ version "1.0.0-rc.3"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.1"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
+chownr@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
+
+ci-info@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cliui@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+ dependencies:
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
+ wrap-ansi "^2.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@^2.19.0, commander@~2.20.0:
+ version "2.20.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
+
+component-emitter@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+convert-source-map@^1.1.0, convert-source-map@^1.4.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
+ dependencies:
+ safe-buffer "~5.1.1"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+cross-spawn@^6.0.0:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-what@2.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad"
+
+cssstyle@^1.0.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.2.tgz#427ea4d585b18624f6fdbf9de7a2a1a3ba713077"
+ dependencies:
+ cssom "0.3.x"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+data-urls@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+ dependencies:
+ abab "^2.0.0"
+ whatwg-mimetype "^2.2.0"
+ whatwg-url "^7.0.0"
+
+debug@^2.2.0, debug@^2.3.3:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.2.6:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.1.0, debug@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ dependencies:
+ ms "^2.1.1"
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ dependencies:
+ object-keys "^1.0.12"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+
+detect-newline@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+
+diff-sequences@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975"
+
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+
+dom-serializer@0, dom-serializer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+
+domexception@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ dependencies:
+ webidl-conversions "^4.0.2"
+
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1, domutils@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+encoding@^0.1.11:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+ dependencies:
+ iconv-lite "~0.4.13"
+
+end-of-stream@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+ dependencies:
+ once "^1.4.0"
+
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+
+enzyme-adapter-react-16@^1.13.2:
+ version "1.14.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.14.0.tgz#204722b769172bcf096cb250d33e6795c1f1858f"
+ dependencies:
+ enzyme-adapter-utils "^1.12.0"
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ object.values "^1.1.0"
+ prop-types "^15.7.2"
+ react-is "^16.8.6"
+ react-test-renderer "^16.0.0-0"
+ semver "^5.7.0"
+
+enzyme-adapter-utils@^1.12.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz#96e3730d76b872f593e54ce1c51fa3a451422d93"
+ dependencies:
+ airbnb-prop-types "^2.13.2"
+ function.prototype.name "^1.1.0"
+ object.assign "^4.1.0"
+ object.fromentries "^2.0.0"
+ prop-types "^15.7.2"
+ semver "^5.6.0"
+
+enzyme-to-json@^3.3.5:
+ version "3.3.5"
+ resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.5.tgz#f8eb82bd3d5941c9d8bc6fd9140030777d17d0af"
+ dependencies:
+ lodash "^4.17.4"
+
+enzyme@^3.9.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.10.0.tgz#7218e347c4a7746e133f8e964aada4a3523452f6"
+ dependencies:
+ array.prototype.flat "^1.2.1"
+ cheerio "^1.0.0-rc.2"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
+ html-element-map "^1.0.0"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.4"
+ is-number-object "^1.0.3"
+ is-regex "^1.0.4"
+ is-string "^1.0.4"
+ is-subset "^0.1.1"
+ lodash.escape "^4.0.1"
+ lodash.isequal "^4.5.0"
+ object-inspect "^1.6.0"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+ object.values "^1.0.4"
+ raf "^3.4.0"
+ rst-selector-parser "^2.2.3"
+ string.prototype.trim "^1.1.2"
+
+error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.5.0, es-abstract@^1.5.1:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ dependencies:
+ es-to-primitive "^1.2.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-keys "^1.0.12"
+
+es-to-primitive@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escodegen@^1.9.1:
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510"
+ dependencies:
+ esprima "^3.1.3"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
+esprima@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
+estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+exec-sh@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b"
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expect@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-24.8.0.tgz#471f8ec256b7b6129ca2524b2a62f030df38718d"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ ansi-styles "^3.2.0"
+ jest-get-type "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-regex-util "^24.3.0"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0, extsprintf@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ dependencies:
+ bser "^2.0.0"
+
+fbjs@^0.8.16:
+ version "0.8.17"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ dependencies:
+ locate-path "^3.0.0"
+
+for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ dependencies:
+ map-cache "^0.2.2"
+
+fs-minipass@^1.2.5:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
+ dependencies:
+ minipass "^2.2.1"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^1.2.7:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f"
+ dependencies:
+ nan "^2.12.1"
+ node-pre-gyp "^0.12.0"
+
+function-bind@^1.0.2, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+function.prototype.name@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ is-callable "^1.1.3"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ dependencies:
+ pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
+ version "7.1.4"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^11.1.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
+ version "4.1.15"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+
+handlebars@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67"
+ dependencies:
+ neo-async "^2.6.0"
+ optimist "^0.6.1"
+ source-map "^0.6.1"
+ optionalDependencies:
+ uglify-js "^3.1.4"
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.1, has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ dependencies:
+ function-bind "^1.1.1"
+
+hosted-git-info@^2.1.4:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
+
+html-element-map@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.0.1.tgz#3c4fcb4874ebddfe4283b51c8994e7713782b592"
+ dependencies:
+ array-filter "^1.0.0"
+
+html-encoding-sniffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+htmlparser2@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ dependencies:
+ domelementtype "^1.3.1"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.1.1"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ dependencies:
+ minimatch "^3.0.4"
+
+import-local@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
+ dependencies:
+ pkg-dir "^3.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+
+invariant@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ dependencies:
+ kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-boolean-object@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
+is-callable@^1.1.3, is-callable@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+
+is-ci@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+ dependencies:
+ ci-info "^2.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-generator-fn@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
+
+is-number-object@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ dependencies:
+ isobject "^3.0.1"
+
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-string@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+
+is-symbol@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+ dependencies:
+ has-symbols "^1.0.0"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+
+isarray@1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
+
+istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
+ dependencies:
+ "@babel/generator" "^7.4.0"
+ "@babel/parser" "^7.4.3"
+ "@babel/template" "^7.4.0"
+ "@babel/traverse" "^7.4.3"
+ "@babel/types" "^7.4.0"
+ istanbul-lib-coverage "^2.0.5"
+ semver "^6.0.0"
+
+istanbul-lib-report@^2.0.4:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33"
+ dependencies:
+ istanbul-lib-coverage "^2.0.5"
+ make-dir "^2.1.0"
+ supports-color "^6.1.0"
+
+istanbul-lib-source-maps@^3.0.1:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8"
+ dependencies:
+ debug "^4.1.1"
+ istanbul-lib-coverage "^2.0.5"
+ make-dir "^2.1.0"
+ rimraf "^2.6.3"
+ source-map "^0.6.1"
+
+istanbul-reports@^2.1.1:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af"
+ dependencies:
+ handlebars "^4.1.2"
+
+jest-changed-files@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ execa "^1.0.0"
+ throat "^4.0.0"
+
+jest-cli@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.8.0.tgz#b075ac914492ed114fa338ade7362a301693e989"
+ dependencies:
+ "@jest/core" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ import-local "^2.0.0"
+ is-ci "^2.0.0"
+ jest-config "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ prompts "^2.0.1"
+ realpath-native "^1.1.0"
+ yargs "^12.0.2"
+
+jest-config@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.8.0.tgz#77db3d265a6f726294687cbbccc36f8a76ee0f4f"
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/test-sequencer" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ babel-jest "^24.8.0"
+ chalk "^2.0.1"
+ glob "^7.1.1"
+ jest-environment-jsdom "^24.8.0"
+ jest-environment-node "^24.8.0"
+ jest-get-type "^24.8.0"
+ jest-jasmine2 "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ micromatch "^3.1.10"
+ pretty-format "^24.8.0"
+ realpath-native "^1.1.0"
+
+jest-diff@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.8.0.tgz#146435e7d1e3ffdf293d53ff97e193f1d1546172"
+ dependencies:
+ chalk "^2.0.1"
+ diff-sequences "^24.3.0"
+ jest-get-type "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-docblock@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd"
+ dependencies:
+ detect-newline "^2.1.0"
+
+jest-each@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.8.0.tgz#a05fd2bf94ddc0b1da66c6d13ec2457f35e52775"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ jest-get-type "^24.8.0"
+ jest-util "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-environment-jsdom@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz#300f6949a146cabe1c9357ad9e9ecf9f43f38857"
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-util "^24.8.0"
+ jsdom "^11.5.1"
+
+jest-environment-node@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.8.0.tgz#d3f726ba8bc53087a60e7a84ca08883a4c892231"
+ dependencies:
+ "@jest/environment" "^24.8.0"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-util "^24.8.0"
+
+jest-get-type@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.8.0.tgz#a7440de30b651f5a70ea3ed7ff073a32dfe646fc"
+
+jest-haste-map@^24.8.0:
+ version "24.8.1"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.8.1.tgz#f39cc1d2b1d907e014165b4bd5a957afcb992982"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ anymatch "^2.0.0"
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.15"
+ invariant "^2.2.4"
+ jest-serializer "^24.4.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ micromatch "^3.1.10"
+ sane "^4.0.3"
+ walker "^1.0.7"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
+jest-jasmine2@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz#a9c7e14c83dd77d8b15e820549ce8987cc8cd898"
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ co "^4.6.0"
+ expect "^24.8.0"
+ is-generator-fn "^2.0.0"
+ jest-each "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ pretty-format "^24.8.0"
+ throat "^4.0.0"
+
+jest-leak-detector@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz#c0086384e1f650c2d8348095df769f29b48e6980"
+ dependencies:
+ pretty-format "^24.8.0"
+
+jest-matcher-utils@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz#2bce42204c9af12bde46f83dc839efe8be832495"
+ dependencies:
+ chalk "^2.0.1"
+ jest-diff "^24.8.0"
+ jest-get-type "^24.8.0"
+ pretty-format "^24.8.0"
+
+jest-message-util@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.8.0.tgz#0d6891e72a4beacc0292b638685df42e28d6218b"
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/stack-utils" "^1.0.1"
+ chalk "^2.0.1"
+ micromatch "^3.1.10"
+ slash "^2.0.0"
+ stack-utils "^1.0.1"
+
+jest-mock@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.8.0.tgz#2f9d14d37699e863f1febf4e4d5a33b7fdbbde56"
+ dependencies:
+ "@jest/types" "^24.8.0"
+
+jest-pnp-resolver@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a"
+
+jest-regex-util@^24.3.0:
+ version "24.3.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.3.0.tgz#d5a65f60be1ae3e310d5214a0307581995227b36"
+
+jest-resolve-dependencies@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz#19eec3241f2045d3f990dba331d0d7526acff8e0"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-snapshot "^24.8.0"
+
+jest-resolve@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.8.0.tgz#84b8e5408c1f6a11539793e2b5feb1b6e722439f"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ browser-resolve "^1.11.3"
+ chalk "^2.0.1"
+ jest-pnp-resolver "^1.2.1"
+ realpath-native "^1.1.0"
+
+jest-runner@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.8.0.tgz#4f9ae07b767db27b740d7deffad0cf67ccb4c5bb"
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.8.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.4.2"
+ exit "^0.1.2"
+ graceful-fs "^4.1.15"
+ jest-config "^24.8.0"
+ jest-docblock "^24.3.0"
+ jest-haste-map "^24.8.0"
+ jest-jasmine2 "^24.8.0"
+ jest-leak-detector "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-resolve "^24.8.0"
+ jest-runtime "^24.8.0"
+ jest-util "^24.8.0"
+ jest-worker "^24.6.0"
+ source-map-support "^0.5.6"
+ throat "^4.0.0"
+
+jest-runtime@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.8.0.tgz#05f94d5b05c21f6dc54e427cd2e4980923350620"
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/environment" "^24.8.0"
+ "@jest/source-map" "^24.3.0"
+ "@jest/transform" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/yargs" "^12.0.2"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.3"
+ graceful-fs "^4.1.15"
+ jest-config "^24.8.0"
+ jest-haste-map "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-mock "^24.8.0"
+ jest-regex-util "^24.3.0"
+ jest-resolve "^24.8.0"
+ jest-snapshot "^24.8.0"
+ jest-util "^24.8.0"
+ jest-validate "^24.8.0"
+ realpath-native "^1.1.0"
+ slash "^2.0.0"
+ strip-bom "^3.0.0"
+ yargs "^12.0.2"
+
+jest-serializer@^24.4.0:
+ version "24.4.0"
+ resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.4.0.tgz#f70c5918c8ea9235ccb1276d232e459080588db3"
+
+jest-snapshot@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.8.0.tgz#3bec6a59da2ff7bc7d097a853fb67f9d415cb7c6"
+ dependencies:
+ "@babel/types" "^7.0.0"
+ "@jest/types" "^24.8.0"
+ chalk "^2.0.1"
+ expect "^24.8.0"
+ jest-diff "^24.8.0"
+ jest-matcher-utils "^24.8.0"
+ jest-message-util "^24.8.0"
+ jest-resolve "^24.8.0"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^24.8.0"
+ semver "^5.5.0"
+
+jest-util@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.8.0.tgz#41f0e945da11df44cc76d64ffb915d0716f46cd1"
+ dependencies:
+ "@jest/console" "^24.7.1"
+ "@jest/fake-timers" "^24.8.0"
+ "@jest/source-map" "^24.3.0"
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ callsites "^3.0.0"
+ chalk "^2.0.1"
+ graceful-fs "^4.1.15"
+ is-ci "^2.0.0"
+ mkdirp "^0.5.1"
+ slash "^2.0.0"
+ source-map "^0.6.0"
+
+jest-validate@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.8.0.tgz#624c41533e6dfe356ffadc6e2423a35c2d3b4849"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ camelcase "^5.0.0"
+ chalk "^2.0.1"
+ jest-get-type "^24.8.0"
+ leven "^2.1.0"
+ pretty-format "^24.8.0"
+
+jest-watcher@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.8.0.tgz#58d49915ceddd2de85e238f6213cef1c93715de4"
+ dependencies:
+ "@jest/test-result" "^24.8.0"
+ "@jest/types" "^24.8.0"
+ "@types/yargs" "^12.0.9"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ jest-util "^24.8.0"
+ string-length "^2.0.0"
+
+jest-worker@^24.6.0:
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.6.0.tgz#7f81ceae34b7cde0c9827a6980c35b7cdc0161b3"
+ dependencies:
+ merge-stream "^1.0.1"
+ supports-color "^6.1.0"
+
+jest@^24.6:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-24.8.0.tgz#d5dff1984d0d1002196e9b7f12f75af1b2809081"
+ dependencies:
+ import-local "^2.0.0"
+ jest-cli "^24.8.0"
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jsdom@^11.5.1:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8"
+ dependencies:
+ abab "^2.0.0"
+ acorn "^5.5.3"
+ acorn-globals "^4.1.0"
+ array-equal "^1.0.0"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle "^1.0.0"
+ data-urls "^1.0.0"
+ domexception "^1.0.1"
+ escodegen "^1.9.1"
+ html-encoding-sniffer "^1.0.2"
+ left-pad "^1.3.0"
+ nwsapi "^2.0.7"
+ parse5 "4.0.0"
+ pn "^1.1.0"
+ request "^2.87.0"
+ request-promise-native "^1.0.5"
+ sax "^1.2.4"
+ symbol-tree "^3.2.2"
+ tough-cookie "^2.3.4"
+ w3c-hr-time "^1.0.1"
+ webidl-conversions "^4.0.2"
+ whatwg-encoding "^1.0.3"
+ whatwg-mimetype "^2.1.0"
+ whatwg-url "^6.4.1"
+ ws "^5.2.0"
+ xml-name-validator "^3.0.0"
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+
+json-parse-better-errors@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json5@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
+ dependencies:
+ minimist "^1.2.0"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
+kleur@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+
+lcid@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
+ dependencies:
+ invert-kv "^2.0.0"
+
+left-pad@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
+
+leven@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+
+levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^4.0.0"
+ pify "^3.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash.escape@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+
+lodash.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+
+lodash.sortby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+
+lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.4:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+make-dir@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+ dependencies:
+ pify "^4.0.1"
+ semver "^5.6.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ dependencies:
+ tmpl "1.0.x"
+
+map-age-cleaner@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+ dependencies:
+ p-defer "^1.0.0"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ dependencies:
+ object-visit "^1.0.0"
+
+mem@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
+ dependencies:
+ map-age-cleaner "^0.1.1"
+ mimic-fn "^2.0.0"
+ p-is-promise "^2.0.0"
+
+merge-stream@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"
+ dependencies:
+ readable-stream "^2.0.1"
+
+micromatch@^3.1.10, micromatch@^3.1.4:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+mime-db@1.40.0:
+ version "1.40.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+ version "2.1.24"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
+ dependencies:
+ mime-db "1.40.0"
+
+mimic-fn@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8, minimist@~0.0.1:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.1.1, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minipass@^2.2.1, minipass@^2.3.5:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614"
+ dependencies:
+ minipass "^2.2.1"
+
+mixin-deep@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@^0.5.0, mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+moo@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+ms@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+
+nan@^2.12.1:
+ version "2.14.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+nearley@^2.7.10:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.16.0.tgz#77c297d041941d268290ec84b739d0ee297e83a7"
+ dependencies:
+ commander "^2.19.0"
+ moo "^0.4.3"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+ semver "^5.4.1"
+
+needle@^2.2.1:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c"
+ dependencies:
+ debug "^3.2.6"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
+neo-async@^2.6.0:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+
+node-modules-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
+
+node-notifier@^5.2.1:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.0.tgz#7b455fdce9f7de0c63538297354f3db468426e6a"
+ dependencies:
+ growly "^1.3.0"
+ is-wsl "^1.1.0"
+ semver "^5.5.0"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+node-pre-gyp@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149"
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.1"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.2.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ resolve "^1.10.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+npm-bundled@^1.0.1:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
+
+npm-packlist@^1.1.6:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc"
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+nth-check@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ dependencies:
+ boolbase "~1.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+nwsapi@^2.0.7:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f"
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+
+object-assign@^4.1.0, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-inspect@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
+
+object-is@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
+
+object-keys@^1.0.11, object-keys@^1.0.12:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ dependencies:
+ isobject "^3.0.0"
+
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.entries@^1.0.4, object.entries@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+object.fromentries@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.11.0"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+
+object.getownpropertydescriptors@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ dependencies:
+ isobject "^3.0.1"
+
+object.values@^1.0.4, object.values@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
+ dependencies:
+ execa "^1.0.0"
+ lcid "^2.0.0"
+ mem "^4.0.0"
+
+os-tmpdir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-defer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+
+p-each-series@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
+ dependencies:
+ p-reduce "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-is-promise@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
+
+p-limit@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2"
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ dependencies:
+ p-limit "^2.0.0"
+
+p-reduce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse5@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
+
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ dependencies:
+ "@types/node" "*"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+
+path-type@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ dependencies:
+ pify "^3.0.0"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+
+pirates@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
+ dependencies:
+ node-modules-regexp "^1.0.0"
+
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ dependencies:
+ find-up "^3.0.0"
+
+pn@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+pretty-format@^24.8.0:
+ version "24.8.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"
+ dependencies:
+ "@jest/types" "^24.8.0"
+ ansi-regex "^4.0.0"
+ ansi-styles "^3.2.0"
+ react-is "^16.8.4"
+
+process-nextick-args@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ dependencies:
+ asap "~2.0.3"
+
+prompts@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.1.0.tgz#bf90bc71f6065d255ea2bdc0fe6520485c1b45db"
+ dependencies:
+ kleur "^3.0.2"
+ sisteransi "^1.0.0"
+
+prop-types-exact@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
+ dependencies:
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ reflect.ownkeys "^0.2.0"
+
+prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
+ version "15.7.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.8.1"
+
+psl@^1.1.24, psl@^1.1.28:
+ version "1.1.32"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.32.tgz#3f132717cf2f9c169724b2b6caf373cf694198db"
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+punycode@^2.1.0, punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+
+raf@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+ dependencies:
+ performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
+rc@^1.2.7:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+react-dom@16:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ scheduler "^0.13.6"
+
+react-is@^16.4.1, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
+
+react-test-renderer@16.4.1, react-test-renderer@^16.0.0-0:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70"
+ dependencies:
+ fbjs "^0.8.16"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+ react-is "^16.4.1"
+
+react@16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+read-pkg-up@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
+ dependencies:
+ find-up "^3.0.0"
+ read-pkg "^3.0.0"
+
+read-pkg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+ dependencies:
+ load-json-file "^4.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^3.0.0"
+
+readable-stream@^2.0.1, readable-stream@^2.0.6:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.1.1:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+realpath-native@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
+ dependencies:
+ util.promisify "^1.0.0"
+
+redux-mock-store@^1.5.3:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d"
+ dependencies:
+ lodash.isplainobject "^4.0.6"
+
+redux@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796"
+ dependencies:
+ loose-envify "^1.4.0"
+ symbol-observable "^1.2.0"
+
+reflect.ownkeys@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+repeat-element@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+
+repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+request-promise-core@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346"
+ dependencies:
+ lodash "^4.17.11"
+
+request-promise-native@^1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59"
+ dependencies:
+ request-promise-core "1.1.2"
+ stealthy-require "^1.1.1"
+ tough-cookie "^2.3.3"
+
+request@^2.87.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
+resolve@^1.10.0, resolve@^1.3.2:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232"
+ dependencies:
+ path-parse "^1.0.6"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+
+rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+ dependencies:
+ glob "^7.1.3"
+
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
+rsvp@^4.8.4:
+ version "4.8.5"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
+sane@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
+ dependencies:
+ "@cnakazawa/watch" "^1.0.3"
+ anymatch "^2.0.0"
+ capture-exit "^2.0.0"
+ exec-sh "^0.3.2"
+ execa "^1.0.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
+scheduler@^0.13.6:
+ version "0.13.6"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0:
+ version "5.7.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
+
+semver@^6.0.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+sisteransi@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c"
+
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+source-map-resolve@^0.5.0:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
+ dependencies:
+ atob "^2.1.1"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@^0.5.6:
+ version "0.5.12"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599"
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+
+source-map@^0.5.0, source-map@^0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+spdx-correct@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1"
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sshpk@^1.7.0:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
+stack-utils@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+stealthy-require@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+
+string-length@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string.prototype.trim@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.0"
+ function-bind "^1.0.2"
+
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+ dependencies:
+ ansi-regex "^4.1.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+ dependencies:
+ has-flag "^3.0.0"
+
+symbol-observable@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+
+symbol-tree@^3.2.2:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+
+tar@^4:
+ version "4.4.10"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1"
+ dependencies:
+ chownr "^1.1.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.5"
+ minizlib "^1.2.1"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.3"
+
+test-exclude@^5.2.3:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"
+ dependencies:
+ glob "^7.1.3"
+ minimatch "^3.0.4"
+ read-pkg-up "^4.0.0"
+ require-main-filename "^2.0.0"
+
+throat@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+tough-cookie@^2.3.3, tough-cookie@^2.3.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+tr46@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+ dependencies:
+ punycode "^2.1.0"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+ua-parser-js@^0.7.18:
+ version "0.7.20"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098"
+
+uglify-js@^3.1.4:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5"
+ dependencies:
+ commander "~2.20.0"
+ source-map "~0.6.1"
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ dependencies:
+ punycode "^2.1.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+util.promisify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ dependencies:
+ define-properties "^1.1.2"
+ object.getownpropertydescriptors "^2.0.3"
+
+uuid@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+w3c-hr-time@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
+ dependencies:
+ browser-process-hrtime "^0.1.2"
+
+walker@^1.0.7, walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ dependencies:
+ makeerror "1.0.x"
+
+webidl-conversions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ dependencies:
+ iconv-lite "0.4.24"
+
+whatwg-fetch@>=0.10.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+
+whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+
+whatwg-url@^6.4.1:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+whatwg-url@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@^1.2.9, which@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+ dependencies:
+ string-width "^1.0.2 || 2"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write-file-atomic@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+ws@^5.2.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
+ dependencies:
+ async-limiter "~1.0.0"
+
+xml-name-validator@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+
+"y18n@^3.2.1 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+
+yallist@^3.0.0, yallist@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
+
+yargs-parser@^11.1.1:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs@^12.0.2:
+ version "12.0.5"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^11.1.1"
diff --git a/devtools/client/application/test/xpcshell/.eslintrc.js b/devtools/client/application/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/client/application/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/application/test/xpcshell/test_manifest_reducer.js b/devtools/client/application/test/xpcshell/test_manifest_reducer.js
new file mode 100644
index 0000000000..2e91b57442
--- /dev/null
+++ b/devtools/client/application/test/xpcshell/test_manifest_reducer.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ FETCH_MANIFEST_FAILURE,
+ FETCH_MANIFEST_START,
+ FETCH_MANIFEST_SUCCESS,
+ RESET_MANIFEST,
+ MANIFEST_MEMBER_VALUE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const { ICON, COLOR, STRING, URL } = MANIFEST_MEMBER_VALUE_TYPES;
+
+const {
+ manifestReducer,
+ ManifestState,
+} = require("resource://devtools/client/application/src/reducers/manifest-state.js");
+
+const MANIFEST_PROCESSING = [
+ // empty manifest
+ {
+ source: {},
+ processed: {},
+ },
+ // manifest with just one member
+ {
+ source: { name: "Foo" },
+ processed: {
+ identity: [{ key: "name", value: "Foo", type: STRING }],
+ },
+ },
+ // manifest with two members from the same category
+ {
+ source: {
+ short_name: "Short Foo",
+ name: "Long Foo",
+ },
+ processed: {
+ identity: [
+ { key: "short_name", value: "Short Foo", type: STRING },
+ { key: "name", value: "Long Foo", type: STRING },
+ ],
+ },
+ },
+ // manifest with members from two different categories
+ {
+ source: {
+ name: "Foo",
+ background_color: "#FF0000",
+ start_url: "https://example.com/?q=foo",
+ scope: "https://example.com",
+ },
+ processed: {
+ identity: [{ key: "name", value: "Foo", type: STRING }],
+ presentation: [
+ { key: "background_color", value: "#FF0000", type: COLOR },
+ { key: "start_url", value: "https://example.com/?q=foo", type: URL },
+ { key: "scope", value: "https://example.com", type: URL },
+ ],
+ },
+ },
+ // manifest with icons
+ {
+ source: {
+ icons: [
+ {
+ src: "something.png",
+ type: "image/png",
+ sizes: ["16x16", "32x32"],
+ purpose: ["any"],
+ },
+ {
+ src: "another.svg",
+ type: "image/svg",
+ sizes: ["any"],
+ purpose: ["any maskable"],
+ },
+ {
+ src: "something.png",
+ type: undefined,
+ sizes: undefined,
+ purpose: ["any"],
+ },
+ ],
+ },
+ processed: {
+ icons: [
+ {
+ key: { sizes: "16x16 32x32", contentType: "image/png" },
+ value: { src: "something.png", purpose: "any" },
+ type: ICON,
+ },
+ {
+ key: { sizes: "any", contentType: "image/svg" },
+ value: { src: "another.svg", purpose: "any maskable" },
+ type: ICON,
+ },
+ {
+ key: { sizes: undefined, contentType: undefined },
+ value: { src: "something.png", purpose: "any" },
+ type: ICON,
+ },
+ ],
+ },
+ },
+ // manifest with issues
+ {
+ source: {
+ moz_validation: [
+ { warn: "A warning" },
+ { error: "An error", type: "json" },
+ ],
+ },
+ processed: {
+ validation: [
+ { level: "warning", message: "A warning", type: null },
+ { level: "error", message: "An error", type: "json" },
+ ],
+ },
+ },
+ // manifest with URL
+ {
+ source: {
+ moz_manifest_url: "https://example.com/manifest.json",
+ },
+ processed: {
+ url: "https://example.com/manifest.json",
+ },
+ },
+];
+
+add_task(async function () {
+ info("Test manifest reducer: FETCH_MANIFEST_START action");
+
+ const state = ManifestState();
+ const action = { type: FETCH_MANIFEST_START };
+ const newState = manifestReducer(state, action);
+
+ equal(newState.isLoading, true, "Loading flag is true");
+});
+
+add_task(async function () {
+ info("Test manifest reducer: FETCH_MANIFEST_FAILURE action");
+
+ const state = Object.assign(ManifestState(), { isLoading: true });
+ const action = { type: FETCH_MANIFEST_FAILURE, error: "some error" };
+ const newState = manifestReducer(state, action);
+
+ equal(newState.errorMessage, "some error", "Error message is as expected");
+ equal(newState.isLoading, false, "Loading flag is false");
+ equal(newState.manifest, null, "Manifest is null");
+});
+
+add_task(async function () {
+ info("Test manifest reducer: FETCH_MANIFEST_SUCCESS action");
+
+ // test manifest processing
+ MANIFEST_PROCESSING.forEach(({ source, processed }) => {
+ test_manifest_processing(source, processed);
+ });
+});
+
+add_task(async function () {
+ info("Test manifest reducer: RESET_MANIFEST action");
+
+ const state = Object.assign(ManifestState(), {
+ isLoading: true,
+ manifest: { identity: [{ key: "name", value: "Foo" }] },
+ errorMessage: "some error",
+ });
+ const action = { type: RESET_MANIFEST };
+ const newState = manifestReducer(state, action);
+
+ deepEqual(newState, ManifestState(), "Manifest has been reset to defaults");
+});
+
+function test_manifest_processing(source, processed) {
+ const state = ManifestState();
+ state.isLoading = true;
+
+ const action = { type: FETCH_MANIFEST_SUCCESS, manifest: source };
+ const newState = manifestReducer(state, action);
+
+ // merge the expected processed manifst with some default values
+ const expected = Object.assign(
+ {
+ icons: [],
+ identity: [],
+ presentation: [],
+ url: undefined,
+ validation: [],
+ },
+ processed
+ );
+
+ deepEqual(newState.manifest, expected, "Processed manifest as expected");
+ equal(newState.errorMessage, "", "Error message is empty");
+ equal(newState.isLoading, false, "Loading flag is false");
+}
diff --git a/devtools/client/application/test/xpcshell/test_page_reducer.js b/devtools/client/application/test/xpcshell/test_page_reducer.js
new file mode 100644
index 0000000000..9aecfc67bb
--- /dev/null
+++ b/devtools/client/application/test/xpcshell/test_page_reducer.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ updateDomain,
+} = require("resource://devtools/client/application/src/actions/page.js");
+
+const {
+ pageReducer,
+ PageState,
+} = require("resource://devtools/client/application/src/reducers/page-state.js");
+
+add_task(async function () {
+ info("Test page reducer: UPDATE_DOMAIN action");
+ const state = PageState();
+ const action = updateDomain("https://example.com/foo/#bar");
+
+ const newState = pageReducer(state, action);
+ equal(newState.domain, "example.com");
+});
diff --git a/devtools/client/application/test/xpcshell/test_ui_reducer.js b/devtools/client/application/test/xpcshell/test_ui_reducer.js
new file mode 100644
index 0000000000..22c1844238
--- /dev/null
+++ b/devtools/client/application/test/xpcshell/test_ui_reducer.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ updateSelectedPage,
+} = require("resource://devtools/client/application/src/actions/ui.js");
+
+const {
+ uiReducer,
+ UiState,
+} = require("resource://devtools/client/application/src/reducers/ui-state.js");
+
+add_task(async function () {
+ info("Test ui reducer: UPDATE_SELECTED_PAGE action");
+ const state = UiState();
+ const action = updateSelectedPage("foo");
+
+ const newState = uiReducer(state, action);
+ equal(newState.selectedPage, "foo");
+});
diff --git a/devtools/client/application/test/xpcshell/test_workers_reducer.js b/devtools/client/application/test/xpcshell/test_workers_reducer.js
new file mode 100644
index 0000000000..a662c313df
--- /dev/null
+++ b/devtools/client/application/test/xpcshell/test_workers_reducer.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ updateCanDebugWorkers,
+ updateWorkers,
+} = require("resource://devtools/client/application/src/actions/workers.js");
+
+const {
+ START_WORKER,
+ UNREGISTER_WORKER,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const {
+ workersReducer,
+ WorkersState,
+} = require("resource://devtools/client/application/src/reducers/workers-state.js");
+
+add_task(async function () {
+ info("Test workers reducer: UPDATE_CAN_DEBUG_WORKERS action");
+
+ function testUpdateCanDebugWorkers(flagValue) {
+ const state = WorkersState();
+ const action = updateCanDebugWorkers(flagValue);
+ const newState = workersReducer(state, action);
+ equal(
+ newState.canDebugWorkers,
+ flagValue,
+ "canDebugWorkers contains the expected value"
+ );
+ }
+
+ testUpdateCanDebugWorkers(false);
+ testUpdateCanDebugWorkers(true);
+});
+
+add_task(async function () {
+ info("Test workers reducer: UPDATE_WORKERS action");
+ const state = WorkersState();
+
+ const rawData = [
+ {
+ registration: {
+ scope: "lorem-ipsum",
+ lastUpdateTime: 42,
+ id: "r1",
+ },
+ workers: [
+ {
+ id: "w1",
+ state: Ci.nsIServiceWorkerInfo.STATE_ACTIVATED,
+ url: "https://example.com/w1.js",
+ workerDescriptorFront: { foo: "bar" },
+ stateText: "activated",
+ },
+ {
+ id: "w2",
+ state: Ci.nsIServiceWorkerInfo.STATE_INSTALLED,
+ url: "https://example.com/w2.js",
+ workerDescriptorFront: undefined,
+ stateText: "installed",
+ },
+ ],
+ },
+ ];
+
+ const expectedData = [
+ {
+ id: "r1",
+ lastUpdateTime: 42,
+ registrationFront: rawData[0].registration,
+ scope: "lorem-ipsum",
+ workers: [
+ {
+ id: "w1",
+ url: "https://example.com/w1.js",
+ workerDescriptorFront: rawData[0].workers[0].workerDescriptorFront,
+ registrationFront: rawData[0].registration,
+ state: Ci.nsIServiceWorkerInfo.STATE_ACTIVATED,
+ stateText: "activated",
+ },
+ {
+ id: "w2",
+ url: "https://example.com/w2.js",
+ workerDescriptorFront: undefined,
+ registrationFront: rawData[0].registration,
+ state: Ci.nsIServiceWorkerInfo.STATE_INSTALLED,
+ stateText: "installed",
+ },
+ ],
+ },
+ ];
+
+ const action = updateWorkers(rawData);
+ const newState = workersReducer(state, action);
+ deepEqual(newState.list, expectedData, "workers contains the expected list");
+});
+
+add_task(async function () {
+ info("Test workers reducer: START_WORKER action");
+ const state = WorkersState();
+ const action = { type: START_WORKER };
+ const newState = workersReducer(state, action);
+ deepEqual(state, newState, "workers state stays the same");
+});
+
+add_task(async function () {
+ info("Test workers reducer: UNREGISTER_WORKER action");
+ const state = WorkersState();
+ const action = { type: UNREGISTER_WORKER };
+ const newState = workersReducer(state, action);
+ deepEqual(state, newState, "workers state stays the same");
+});
diff --git a/devtools/client/application/test/xpcshell/xpcshell-head.js b/devtools/client/application/test/xpcshell/xpcshell-head.js
new file mode 100644
index 0000000000..733c0400da
--- /dev/null
+++ b/devtools/client/application/test/xpcshell/xpcshell-head.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
diff --git a/devtools/client/application/test/xpcshell/xpcshell.toml b/devtools/client/application/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..1cf00f01c1
--- /dev/null
+++ b/devtools/client/application/test/xpcshell/xpcshell.toml
@@ -0,0 +1,13 @@
+[DEFAULT]
+tags = "devtools"
+head = "xpcshell-head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+
+["test_manifest_reducer.js"]
+
+["test_page_reducer.js"]
+
+["test_ui_reducer.js"]
+
+["test_workers_reducer.js"]