diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/netmonitor/test | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/netmonitor/test')
304 files changed, 34316 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/test/.eslintrc.js b/devtools/client/netmonitor/test/.eslintrc.js new file mode 100644 index 0000000000..d6776ad3f7 --- /dev/null +++ b/devtools/client/netmonitor/test/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + env: { + jest: true, + }, +}; diff --git a/devtools/client/netmonitor/test/OstrichLicense.txt b/devtools/client/netmonitor/test/OstrichLicense.txt new file mode 100644 index 0000000000..14c043d601 --- /dev/null +++ b/devtools/client/netmonitor/test/OstrichLicense.txt @@ -0,0 +1,41 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file diff --git a/devtools/client/netmonitor/test/browser.toml b/devtools/client/netmonitor/test/browser.toml new file mode 100644 index 0000000000..00f6ac2068 --- /dev/null +++ b/devtools/client/netmonitor/test/browser.toml @@ -0,0 +1,551 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +skip-if = [ + "http3", # Bug 1829298 + "http2", +] +support-files = [ + "dropmarker.svg", + "file_ws_backend_wsh.py", + "head.js", + "html_cause-test-page.html", + "html_content-type-without-cache-test-page.html", + "html_brotli-test-page.html", + "html_image-tooltip-test-page.html", + "html_cache-test-page.html", + "html_cors-test-page.html", + "html_csp-frame-test-page.html", + "html_csp-resend-test-page.html", + "html_csp-test-page.html", + "html_custom-get-page.html", + "html_cyrillic-test-page.html", + "html_fonts-test-page.html", + "html_frame-test-page.html", + "html_frame-subdocument.html", + "html_filter-test-page.html", + "html_infinite-get-page.html", + "html_internal-stylesheet.html", + "html_json-b64.html", + "html_json-basic.html", + "html_json-custom-mime-test-page.html", + "html_json-empty.html", + "html_json-long-test-page.html", + "html_json-malformed-test-page.html", + "html_json-text-mime-test-page.html", + "html_json-xssi-protection.html", + "html_jsonp-test-page.html", + "html_maps-test-page.html", + "html_navigate-test-page.html", + "html_params-test-page.html", + "html_pause-test-page.html", + "html_post-data-test-page.html", + "html_post-array-data-test-page.html", + "html_post-json-test-page.html", + "html_post-raw-test-page.html", + "html_header-test-page.html", + "html_post-raw-with-headers-test-page.html", + "html_simple-test-page.html", + "html_single-get-page.html", + "html_slow-requests-test-page.html", + "html_send-beacon.html", + "html_sorting-test-page.html", + "html_statistics-edge-case-page.html", + "html_statistics-test-page.html", + "html_status-codes-test-page.html", + "html_tracking-protection.html", + "html_api-calls-test-page.html", + "html_copy-as-curl.html", + "html_curl-utils.html", + "html_open-request-in-tab.html", + "html_worker-test-page.html", + "html_websocket-test-page.html", + "html_ws-early-connection-page.html", + "html_ws-test-page.html", + "html_sse-test-page.html", + "html_ws-sse-test-page.html", + "html_image-cache.html", + "js_worker-test.js", + "js_worker-test2.js", + "js_websocket-worker-test.js", + "sjs_content-type-test-server.sjs", + "sjs_cors-test-server.sjs", + "sjs_https-redirect-test-server.sjs", + "sjs_hsts-test-server.sjs", + "sjs_json-test-server.sjs", + "sjs_long-polling-server.sjs", + "sjs_search-test-server.sjs", + "sjs_method-test-server.sjs", + "sjs_set-cookie-same-site.sjs", + "sjs_simple-test-server.sjs", + "sjs_simple-unsorted-cookies-test-server.sjs", + "sjs_slow-script-server.sjs", + "sjs_slow-test-server.sjs", + "sjs_sorting-test-server.sjs", + "sjs_sse-test-server.sjs", + "sjs_status-codes-test-server.sjs", + "sjs_timings-test-server.sjs", + "sjs_truncate-test-server.sjs", + "test-image.png", + "ostrich-regular.ttf", + "ostrich-black.ttf", + "service-workers/status-codes.html", + "service-workers/status-codes-service-worker.js", + "xhr_bundle.js", + "xhr_bundle.js.map", + "xhr_original.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/webconsole/test/browser/shared-head.js", +] + +["browser_net-ws-filter-freetext.js"] + +["browser_net_accessibility-01.js"] + +["browser_net_accessibility-02.js"] + +["browser_net_api-calls.js"] + +["browser_net_autoscroll.js"] + +["browser_net_background_update.js"] + +["browser_net_basic-search.js"] + +["browser_net_block-context.js"] + +["browser_net_block-csp.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_block-draganddrop.js"] + +["browser_net_block-extensions.js"] + +["browser_net_block-pattern.js"] +skip-if = [ + "os == 'linux'", + "debug && os == 'win'", # Bug 1603355 + "debug && os == 'mac'", # Bug 1603355 +] + +["browser_net_block-serviceworker.js"] + +["browser_net_block.js"] + +["browser_net_brotli.js"] + +["browser_net_cache_details.js"] + +["browser_net_cached-status.js"] +skip-if = [ + "verify", + "win11_2009", # Bug 1797751 +] + +["browser_net_cause_redirect.js"] + +["browser_net_cause_source_map.js"] + +["browser_net_charts-01.js"] + +["browser_net_charts-02.js"] + +["browser_net_charts-03.js"] + +["browser_net_charts-04.js"] + +["browser_net_charts-05.js"] + +["browser_net_charts-06.js"] + +["browser_net_charts-07.js"] + +["browser_net_clear.js"] + +["browser_net_column-resize-fit.js"] + +["browser_net_column_headers_tooltips.js"] + +["browser_net_column_slow-request-indicator.js"] + +["browser_net_columns_last_column.js"] + +["browser_net_columns_pref.js"] + +["browser_net_columns_reset.js"] + +["browser_net_columns_showhide.js"] + +["browser_net_columns_time.js"] +skip-if = [ + "win11_2009", # Bug 1797751 +] + +["browser_net_complex-params.js"] +skip-if = [ + "verify && !debug && os == 'win'", + "win11_2009", # Bug 1797751 +] + +["browser_net_content-type.js"] +skip-if = ["!debug && os == 'mac'"] + +["browser_net_cookies_sorted.js"] +skip-if = ["verify && debug && os == 'win'"] + +["browser_net_copy_as_curl.js"] + +["browser_net_copy_as_fetch.js"] + +["browser_net_copy_as_powershell.js"] + +["browser_net_copy_headers.js"] + +["browser_net_copy_image_as_data_uri.js"] + +["browser_net_copy_params.js"] +skip-if = [ + "win11_2009", # Bug 1797751 + "verify && !debug && os == 'mac'", # bug 1328915, disable linux32 debug devtools for timeouts +] + +["browser_net_copy_response.js"] + +["browser_net_copy_svg_image_as_data_uri.js"] + +["browser_net_copy_url.js"] + +["browser_net_cors_requests.js"] + +["browser_net_curl-utils.js"] + +["browser_net_cyrillic-01.js"] + +["browser_net_cyrillic-02.js"] + +["browser_net_decode-params.js"] + +["browser_net_decode-url.js"] + +["browser_net_details_copy.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_domain-not-found.js"] + +["browser_net_edit_resend_cancel.js"] + +["browser_net_edit_resend_caret.js"] + +["browser_net_edit_resend_with_filtering.js"] + +["browser_net_edit_resend_xhr.js"] + +["browser_net_error-boundary-01.js"] + +["browser_net_filter-01.js"] + +["browser_net_filter-02.js"] + +["browser_net_filter-03.js"] + +["browser_net_filter-04.js"] + +["browser_net_filter-autocomplete.js"] + +["browser_net_filter-flags.js"] + +["browser_net_filter-sts-search.js"] + +["browser_net_filter-value-preserved.js"] + +["browser_net_fission_switch_target.js"] +skip-if = ["verify"] # Bug 1607678 + +["browser_net_fonts.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_footer-summary.js"] + +["browser_net_frame.js"] +skip-if = ["true"] # Bug 1479782 + +["browser_net_header-docs.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_header-ref-policy.js"] + +["browser_net_header-request-priority.js"] + +["browser_net_headers-alignment.js"] + +["browser_net_headers-link_clickable.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_headers-proxy.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_headers-resize.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_headers_filter.js"] + +["browser_net_headers_sorted.js"] + +["browser_net_html-preview.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_image-tooltip.js"] + +["browser_net_image_cache.js"] + +["browser_net_initiator.js"] + +["browser_net_internal-stylesheet.js"] + +["browser_net_json-b64.js"] + +["browser_net_json-empty.js"] + +["browser_net_json-long.js"] + +["browser_net_json-malformed.js"] + +["browser_net_json-nogrip.js"] + +["browser_net_json-null.js"] + +["browser_net_json-xssi-protection.js"] + +["browser_net_json_custom_mime.js"] + +["browser_net_json_text_mime.js"] + +["browser_net_jsonp.js"] + +["browser_net_large-response.js"] + +["browser_net_leak_on_tab_close.js"] + +["browser_net_new_request_panel.js"] + +["browser_net_new_request_panel_clear_button.js"] + +["browser_net_new_request_panel_content-length.js"] + +["browser_net_new_request_panel_context_menu.js"] +skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results + +["browser_net_new_request_panel_persisted_content.js"] + +["browser_net_new_request_panel_send_request.js"] + +["browser_net_new_request_panel_sync_url_params.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_offline_mode.js"] +skip-if = ["!fission"] + +["browser_net_open_in_debugger.js"] + +["browser_net_open_in_style_editor.js"] + +["browser_net_open_request_in_tab.js"] + +["browser_net_pane-collapse.js"] + +["browser_net_pane-network-details.js"] + +["browser_net_pane-toggle.js"] + +["browser_net_params_sorted.js"] + +["browser_net_pause.js"] +skip-if = ["verify && debug && os == 'win'"] + +["browser_net_persistent_logs.js"] +skip-if = ["true"] #Bug 1661612 + +["browser_net_post-data-json-payloads.js"] + +["browser_net_post-data-raw-payloads-with-upload-stream-headers.js"] + +["browser_net_post-data-raw-payloads.js"] + +["browser_net_post-data.js"] + +["browser_net_prefs-and-l10n.js"] + +["browser_net_prefs-reload.js"] +skip-if = ["os == 'win'"] # bug 1391264 + +["browser_net_raw_headers.js"] + +["browser_net_reload-button.js"] + +["browser_net_req-resp-bodies.js"] + +["browser_net_resend.js"] + +["browser_net_resend_cors.js"] + +["browser_net_resend_csp.js"] + +["browser_net_resend_headers.js"] + +["browser_net_resend_hidden_headers.js"] + +["browser_net_resend_xhr.js"] + +["browser_net_response_CORS_blocked.js"] +skip-if = ["a11y_checks && os == 'linux' && !debug"] # bug 1732635 + +["browser_net_response_node-expanded.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_save_response_as.js"] + +["browser_net_search-results.js"] +skip-if = ["os == 'linux' && a11y_checks"] # Bug 1721160 + +["browser_net_security-details.js"] + +["browser_net_security-error.js"] + +["browser_net_security-icon-click.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_security-redirect.js"] + +["browser_net_security-state.js"] + +["browser_net_security-tab-deselect.js"] + +["browser_net_security-tab-visibility.js"] + +["browser_net_security-warnings.js"] + +["browser_net_send-beacon-other-tab.js"] + +["browser_net_send-beacon.js"] + +["browser_net_server_timings.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_service-worker-status.js"] +skip-if = ["verify && !debug && os == 'linux'"] + +["browser_net_service-worker-timings.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_set-cookie-same-site.js"] + +["browser_net_simple-request-data.js"] +skip-if = ["true"] #Bug 1667115 + +["browser_net_simple-request-details.js"] + +["browser_net_simple-request.js"] + +["browser_net_sort-01.js"] + +["browser_net_sort-02.js"] + +["browser_net_sort-reset.js"] + +["browser_net_sse-basic.js"] + +["browser_net_stacktraces-visibility.js"] + +["browser_net_statistics-01.js"] +skip-if = ["true"] # Bug 1373558 + +["browser_net_statistics-02.js"] + +["browser_net_statistics-edge-case.js"] + +["browser_net_status-bar-transferred-size.js"] + +["browser_net_status-bar.js"] + +["browser_net_status-codes.js"] + +["browser_net_streaming-response.js"] + +["browser_net_tabbar_focus.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_telemetry_edit_resend.js"] + +["browser_net_telemetry_filters_changed.js"] + +["browser_net_telemetry_persist_toggle_changed.js"] + +["browser_net_telemetry_select_ws_frame.js"] + +["browser_net_telemetry_sidepanel_changed.js"] + +["browser_net_telemetry_throttle_changed.js"] + +["browser_net_throttle.js"] + +["browser_net_throttling_profiles.js"] + +["browser_net_timeline_ticks.js"] +skip-if = ["true"] # TODO: fix the test + +["browser_net_timing-division.js"] + +["browser_net_tracking-resources.js"] + +["browser_net_truncate-post-data.js"] +skip-if = ["socketprocess_networking"] # Bug 1772211 + +["browser_net_truncate.js"] + +["browser_net_url-preview.js"] +fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled + +["browser_net_use_as_fetch.js"] + +["browser_net_view-source-debugger.js"] + +["browser_net_waterfall-click.js"] + +["browser_net_websocket_stacks.js"] + +["browser_net_worker_stacks.js"] + +["browser_net_ws-basic.js"] + +["browser_net_ws-clear.js"] + +["browser_net_ws-connection-closed.js"] + +["browser_net_ws-copy-binary-message.js"] + +["browser_net_ws-early-connection.js"] + +["browser_net_ws-filter-dropdown.js"] + +["browser_net_ws-filter-regex.js"] + +["browser_net_ws-json-action-cable-payload.js"] + +["browser_net_ws-json-payload.js"] + +["browser_net_ws-json-stomp-payload.js"] + +["browser_net_ws-keep-future-frames.js"] + +["browser_net_ws-limit-frames.js"] + +["browser_net_ws-limit-payload.js"] + +["browser_net_ws-messages-navigation.js"] + +["browser_net_ws-sockjs-stomp-payload.js"] + +["browser_net_ws-sse-persist-columns.js"] + +["browser_net_ws-stomp-payload.js"] diff --git a/devtools/client/netmonitor/test/browser_http3.toml b/devtools/client/netmonitor/test/browser_http3.toml new file mode 100644 index 0000000000..66bea61f79 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_http3.toml @@ -0,0 +1,14 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +run-if = ["http3"] +support-files = [ + "head.js", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", + "!/devtools/client/webconsole/test/browser/shared-head.js", +] + +["browser_net_header-dns.js"] + +["browser_net_http3_request_details.js"] diff --git a/devtools/client/netmonitor/test/browser_net-ws-filter-freetext.js b/devtools/client/netmonitor/test/browser_net-ws-filter-freetext.js new file mode 100644 index 0000000000..3b17e8c8be --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net-ws-filter-freetext.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS connection is established successfully and filtering messages using freetext works correctly. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedMessages } = windowRequire( + "devtools/client/netmonitor/src/selectors/messages" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connection(s) to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 2); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(3); + await content.wrappedJSObject.openConnection(1); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 2, "There should be two requests"); + + // Wait for all sent/received messages to be displayed in DevTools + const wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 6 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 6, "There should be six frames"); + + // Fill filter input with text and check displayed messages + const filterInput = document.querySelector( + "#messages-view .devtools-filterinput" + ); + filterInput.focus(); + typeInNetmonitor("Payload 2", monitor); + + // Wait till the text filter is applied. + await waitUntil(() => getDisplayedMessages(store.getState()).length == 2); + + const filteredFrames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + is(filteredFrames.length, 2, "There should be two frames"); + + // Select the second request and check that the filter input is cleared + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[1]); + // Wait till the text filter is applied. There should be two frames rendered + await waitUntil( + () => + document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ).length == 2 + ); + const secondRequestFrames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + is(secondRequestFrames.length, 2, "There should be two frames"); + is(filterInput.value, "", "The filter input is cleared"); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_accessibility-01.js b/devtools/client/netmonitor/test/browser_net_accessibility-01.js new file mode 100644 index 0000000000..58ac0c91fe --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_accessibility-01.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if focus modifiers work for the Side Menu. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + // It seems that this test may be slow on Ubuntu builds running on ec2. + requestLongerTimeout(2); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + let count = 0; + function check(selectedIndex, panelVisibility) { + info("Performing check " + count++ + "."); + + const requestItems = Array.from( + document.querySelectorAll(".request-list-item") + ); + is( + requestItems.findIndex(item => item.matches(".selected")), + selectedIndex, + "The selected item in the requests menu was incorrect." + ); + is( + !!document.querySelector(".network-details-bar"), + panelVisibility, + "The network details panel should render correctly." + ); + } + + // Execute requests. + await performRequests(monitor, tab, 2); + + check(-1, false); + + store.dispatch(Actions.selectDelta(+Infinity)); + check(1, true); + store.dispatch(Actions.selectDelta(-Infinity)); + check(0, true); + + store.dispatch(Actions.selectDelta(+1)); + check(1, true); + store.dispatch(Actions.selectDelta(-1)); + check(0, true); + + store.dispatch(Actions.selectDelta(+10)); + check(1, true); + store.dispatch(Actions.selectDelta(-10)); + check(0, true); + + // Execute requests. + await performRequests(monitor, tab, 18); + + store.dispatch(Actions.selectDelta(+Infinity)); + check(19, true); + store.dispatch(Actions.selectDelta(-Infinity)); + check(0, true); + + store.dispatch(Actions.selectDelta(+1)); + check(1, true); + store.dispatch(Actions.selectDelta(-1)); + check(0, true); + + store.dispatch(Actions.selectDelta(+10)); + check(10, true); + store.dispatch(Actions.selectDelta(-10)); + check(0, true); + + store.dispatch(Actions.selectDelta(+100)); + check(19, true); + store.dispatch(Actions.selectDelta(-100)); + check(0, true); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_accessibility-02.js b/devtools/client/netmonitor/test/browser_net_accessibility-02.js new file mode 100644 index 0000000000..003f278194 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_accessibility-02.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if keyboard and mouse navigation works in the network requests menu. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + // It seems that this test may be slow on Ubuntu builds running on ec2. + requestLongerTimeout(2); + + const { window, document, windowRequire, store } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + let count = 0; + function check(selectedIndex, panelVisibility) { + info("Performing check " + count++ + "."); + + const requestItems = Array.from( + document.querySelectorAll(".request-list-item") + ); + is( + requestItems.findIndex(item => item.matches(".selected")), + selectedIndex, + "The selected item in the requests menu was incorrect." + ); + is( + !!document.querySelector(".network-details-bar"), + panelVisibility, + "The network details panel should render correctly." + ); + } + + // Execute requests. + await performRequests(monitor, tab, 2); + + document.querySelector(".requests-list-row-group").focus(); + + check(-1, false); + + EventUtils.sendKey("DOWN", window); + check(0, true); + EventUtils.sendKey("UP", window); + check(0, true); + + EventUtils.sendKey("PAGE_DOWN", window); + check(1, true); + EventUtils.sendKey("PAGE_UP", window); + check(0, true); + + EventUtils.sendKey("END", window); + check(1, true); + EventUtils.sendKey("HOME", window); + check(0, true); + + // Execute requests. + await performRequests(monitor, tab, 18); + + EventUtils.sendKey("DOWN", window); + check(1, true); + EventUtils.sendKey("DOWN", window); + check(2, true); + EventUtils.sendKey("UP", window); + check(1, true); + EventUtils.sendKey("UP", window); + check(0, true); + + EventUtils.sendKey("PAGE_DOWN", window); + check(4, true); + EventUtils.sendKey("PAGE_DOWN", window); + check(8, true); + EventUtils.sendKey("PAGE_UP", window); + check(4, true); + EventUtils.sendKey("PAGE_UP", window); + check(0, true); + + EventUtils.sendKey("HOME", window); + check(0, true); + EventUtils.sendKey("HOME", window); + check(0, true); + EventUtils.sendKey("PAGE_UP", window); + check(0, true); + EventUtils.sendKey("HOME", window); + check(0, true); + + EventUtils.sendKey("END", window); + check(19, true); + EventUtils.sendKey("END", window); + check(19, true); + EventUtils.sendKey("PAGE_DOWN", window); + check(19, true); + EventUtils.sendKey("END", window); + check(19, true); + + EventUtils.sendKey("PAGE_UP", window); + check(15, true); + EventUtils.sendKey("PAGE_UP", window); + check(11, true); + EventUtils.sendKey("UP", window); + check(10, true); + EventUtils.sendKey("UP", window); + check(9, true); + EventUtils.sendKey("PAGE_DOWN", window); + check(13, true); + EventUtils.sendKey("PAGE_DOWN", window); + check(17, true); + EventUtils.sendKey("PAGE_DOWN", window); + check(19, true); + EventUtils.sendKey("PAGE_DOWN", window); + check(19, true); + + EventUtils.sendKey("HOME", window); + check(0, true); + EventUtils.sendKey("DOWN", window); + check(1, true); + EventUtils.sendKey("END", window); + check(19, true); + EventUtils.sendKey("DOWN", window); + check(19, true); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelector(".request-list-item") + ); + check(0, true); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_api-calls.js b/devtools/client/netmonitor/test/browser_net_api-calls.js new file mode 100644 index 0000000000..71b178a97e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_api-calls.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether API call URLs (without a filename) are correctly displayed + * (including Unicode) + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(API_CALLS_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const REQUEST_URIS = [ + "https://example.com/api/fileName.xml", + "https://example.com/api/file%E2%98%A2.xml", + "https://example.com/api/ascii/get/", + "https://example.com/api/unicode/%E2%98%A2/", + "https://example.com/api/search/?q=search%E2%98%A2", + ]; + + // Execute requests. + await performRequests(monitor, tab, 5); + + REQUEST_URIS.forEach(function (uri, index) { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[index], + "GET", + uri + ); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_autoscroll.js b/devtools/client/netmonitor/test/browser_net_autoscroll.js new file mode 100644 index 0000000000..0614641d77 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_autoscroll.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 863102 - Automatically scroll down upon new network requests. + * edited to account for changes made to fix Bug 1360457 + */ +add_task(async function () { + requestLongerTimeout(4); + + const { tab, monitor } = await initNetMonitor(INFINITE_GET_URL, { + enableCache: true, + requestCount: 1, + }); + const { document, windowRequire, store } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait until the first request makes the empty notice disappear + await waitForRequestListToAppear(); + + const requestsContainer = document.querySelector(".requests-list-scroll"); + ok(requestsContainer, "Container element exists as expected."); + + // (1) Check that the scroll position is maintained at the bottom + // when the requests overflow the vertical size of the container. + await waitForRequestsToOverflowContainer(); + await waitForScroll(); + ok(true, "Scrolled to bottom on overflow."); + + // (2) Now scroll to the top and check that additional requests + // do not change the scroll position. + requestsContainer.scrollTop = 0; + await waitSomeTime(); + ok(!scrolledToBottom(requestsContainer), "Not scrolled to bottom."); + // save for comparison later + let { scrollTop } = requestsContainer; + // As we are scrolled top, new request appended won't be fetching their event timings, + // so do not wait for them + await waitForNetworkEvents(monitor, 8, { expectedEventTimings: 0 }); + await waitSomeTime(); + is(requestsContainer.scrollTop, scrollTop, "Did not scroll."); + + // (3) Now set the scroll position back at the bottom and check that + // additional requests *do* cause the container to scroll down. + requestsContainer.scrollTop = requestsContainer.scrollHeight; + ok(scrolledToBottom(requestsContainer), "Set scroll position to bottom."); + await waitForNetworkEvents(monitor, 8); + await waitForScroll(); + ok(true, "Still scrolled to bottom."); + + // (4) Now select the first item in the list + // and check that additional requests do not change the scroll position + // from just below the headers. + store.dispatch(Actions.selectRequestByIndex(0)); + scrollTop = requestsContainer.scrollTop; + await waitForNetworkEvents(monitor, 8); + await waitSomeTime(); + is(requestsContainer.scrollTop, scrollTop, "Did not scroll."); + + // Stop doing requests. + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + content.wrappedJSObject.stopRequests(); + }); + + // Done: clean up. + return teardown(monitor); + + function waitForRequestListToAppear() { + info( + "Waiting until the empty notice disappears and is replaced with the list" + ); + return waitUntil( + () => !!document.querySelector(".requests-list-row-group") + ); + } + + async function waitForRequestsToOverflowContainer() { + info("Waiting for enough requests to overflow the container"); + while (true) { + info("Waiting for one network request"); + await waitForNetworkEvents(monitor, 1); + if ( + requestsContainer.scrollHeight > + requestsContainer.clientHeight + 50 + ) { + info("The list is long enough, returning"); + return; + } + } + } + + function scrolledToBottom(element) { + return element.scrollTop + element.clientHeight >= element.scrollHeight; + } + + function waitSomeTime() { + // Wait to make sure no scrolls happen + return wait(50); + } + + function waitForScroll() { + info("Waiting for the list to scroll to bottom"); + return waitUntil(() => scrolledToBottom(requestsContainer)); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_background_update.js b/devtools/client/netmonitor/test/browser_net_background_update.js new file mode 100644 index 0000000000..e46db87a79 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_background_update.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that network logs created when the Net panel is not visible + * are displayed when the user shows the panel again. + */ +add_task(async () => { + const { tab, monitor, toolbox } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute two requests + await performRequests(monitor, tab, 2); + + // Wait for two logs + await waitUntil( + () => document.querySelectorAll(".request-list-item").length == 2 + ); + + info("Select the inspector"); + await toolbox.selectTool("inspector"); + + info("Wait for Net panel to be hidden"); + await waitUntil(() => document.visibilityState == "hidden"); + + // Execute another two requests + await performRequests(monitor, tab, 2); + + // The number of rendered requests should be the same since + // requests shouldn't be rendered while the net panel is in + // background + is( + document.querySelectorAll(".request-list-item").length, + 2, + "There should be expected number of requests" + ); + + info("Select the Net panel again"); + await toolbox.selectTool("netmonitor"); + + // Wait for another two logs to be rendered since the panel + // is selected now. + await waitUntil( + () => document.querySelectorAll(".request-list-item").length == 4 + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_basic-search.js b/devtools/client/netmonitor/test/browser_net_basic-search.js new file mode 100644 index 0000000000..b7e00fdd5f --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_basic-search.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test basic search functionality. + * Search panel is visible and number of expected results are returned. + */ + +add_task(async function () { + await pushPref("devtools.netmonitor.features.search", true); + + const { tab, monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Execute two XHRs (the same URL) and wait till it's finished. + const URL = HTTPS_SEARCH_SJS + "?value=test"; + const wait = waitForNetworkEvents(monitor, 2); + + await SpecialPowers.spawn(tab.linkedBrowser, [URL], async function (url) { + content.wrappedJSObject.performRequests(2, url); + }); + await wait; + + // Open the Search panel + store.dispatch(Actions.openSearch()); + + // Fill Filter input with text and check displayed messages. + // The filter should be focused automatically. + typeInNetmonitor("test", monitor); + EventUtils.synthesizeKey("KEY_Enter"); + + // Wait till there are two resources rendered in the results. + await waitForDOMIfNeeded( + document, + ".search-panel-content .treeRow.resourceRow", + 2 + ); + + // Click on the first resource to expand it + AccessibilityUtils.setEnv({ + // Keyboard users use arrow keys to expand/collapse tree items. + // Accessibility is handled on the container level. + mustHaveAccessibleRule: false, + }); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".search-panel-content .treeRow .treeIcon") + ); + AccessibilityUtils.resetEnv(); + + // Check that there is 5 matches. + const matches = document.querySelectorAll( + ".search-panel-content .treeRow.resultRow" + ); + is(matches.length, 5, "There must be 5 matches"); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_block-context.js b/devtools/client/netmonitor/test/browser_net_block-context.js new file mode 100644 index 0000000000..390df67837 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_block-context.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that context menus for blocked requests work + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + requestLongerTimeout(2); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + info("Loading initial page"); + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + info("Opening the blocked requests panel"); + document.querySelector(".requests-list-blocking-button").click(); + + info("Adding sample block strings"); + const waitForBlockingContents = waitForDOM( + document, + ".request-blocking-contents" + ); + await waitForBlockingAction(store, () => Actions.addBlockedUrl("test-page")); + await waitForBlockingAction(store, () => Actions.addBlockedUrl("Two")); + await waitForBlockingContents; + + is(getListitems(document), 2); + + info("Reloading page, URLs should be blocked in request list"); + await reloadPage(monitor, { isRequestBlocked: true }); + is(checkIfRequestIsBlocked(document), true); + + info("Disabling all blocked strings"); + await openMenuAndClick( + monitor, + store, + document, + "request-blocking-disable-all" + ); + is(getCheckedCheckboxes(document), 0); + + info("Reloading page, URLs should not be blocked in request list"); + await reloadPage(monitor, { isRequestBlocked: false }); + + is(checkIfRequestIsBlocked(document), false); + + info("Enabling all blocked strings"); + await openMenuAndClick( + monitor, + store, + document, + "request-blocking-enable-all" + ); + is(getCheckedCheckboxes(document), 2); + + info("Reloading page, URLs should be blocked in request list"); + await reloadPage(monitor, { isRequestBlocked: true }); + + is(checkIfRequestIsBlocked(document), true); + + info("Removing all blocked strings"); + await openMenuAndClick( + monitor, + store, + document, + "request-blocking-remove-all" + ); + is(getListitems(document), 0); + + info("Reloading page, URLs should no longer be blocked in request list"); + await reloadPage(monitor, { isRequestBlocked: false }); + is(checkIfRequestIsBlocked(document), false); + + return teardown(monitor); +}); + +async function waitForBlockingAction(store, action) { + const wait = waitForDispatch(store, "REQUEST_BLOCKING_UPDATE_COMPLETE"); + store.dispatch(action()); + await wait; +} + +async function openMenuAndClick(monitor, store, document, itemSelector) { + info(`Right clicking on white-space in the header to get the context menu`); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector(".request-blocking-contents") + ); + + const wait = waitForDispatch(store, "REQUEST_BLOCKING_UPDATE_COMPLETE"); + await selectContextMenuItem(monitor, itemSelector); + await wait; +} + +async function reloadPage(monitor, { isRequestBlocked = false } = {}) { + const wait = waitForNetworkEvents(monitor, 1); + if (isRequestBlocked) { + // Note: Do not use navigateTo or reloadBrowser here as the request will + // be blocked and no navigation happens + gBrowser.selectedBrowser.reload(); + } else { + await reloadBrowser(); + } + await wait; +} + +function getCheckedCheckboxes(document) { + const checkboxes = [ + ...document.querySelectorAll(".request-blocking-contents li input"), + ]; + return checkboxes.filter(checkbox => checkbox.checked).length; +} + +function getListitems(document) { + return document.querySelectorAll(".request-blocking-contents li").length; +} + +function checkIfRequestIsBlocked(document) { + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + const blockedRequestSize = firstRequest.querySelector( + ".requests-list-transferred" + ).textContent; + return blockedRequestSize.includes("Blocked"); +} diff --git a/devtools/client/netmonitor/test/browser_net_block-csp.js b/devtools/client/netmonitor/test/browser_net_block-csp.js new file mode 100644 index 0000000000..f4947cd769 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_block-csp.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that CSP violations display in the netmonitor when blocked + */ + +add_task(async function () { + info("Test requests blocked by CSP in the top level document"); + await testRequestsBlockedByCSP( + HTTPS_EXAMPLE_URL, + HTTPS_EXAMPLE_URL + "html_csp-test-page.html" + ); + + // The html_csp-frame-test-page.html (in the .com domain) includes + // an iframe from the .org domain + info("Test requests blocked by CSP in remote frames"); + await testRequestsBlockedByCSP( + HTTPS_EXAMPLE_ORG_URL, + HTTPS_EXAMPLE_URL + "html_csp-frame-test-page.html" + ); +}); + +async function testRequestsBlockedByCSP(baseUrl, page) { + const { monitor } = await initNetMonitor(page, { requestCount: 3 }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + const scriptFileName = "js_websocket-worker-test.js"; + const styleFileName = "internal-loaded.css"; + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 3); + await reloadBrowser(); + info("Waiting until the requests appear in netmonitor"); + await wait; + + const displayedRequests = getDisplayedRequests(store.getState()); + + const styleRequest = displayedRequests.find(request => + request.url.includes(styleFileName) + ); + + info("Ensure the attempt to load a CSS file shows a blocked CSP error"); + + verifyRequestItemTarget( + document, + displayedRequests, + styleRequest, + "GET", + baseUrl + styleFileName, + { + transferred: "CSP", + cause: { type: "stylesheet" }, + type: "", + } + ); + + const scriptRequest = displayedRequests.find(request => + request.url.includes(scriptFileName) + ); + + info("Test that the attempt to load a JS file shows a blocked CSP error"); + + verifyRequestItemTarget( + document, + displayedRequests, + scriptRequest, + "GET", + baseUrl + scriptFileName, + { + transferred: "CSP", + cause: { type: "script" }, + type: "", + } + ); + + info("Test that header infomation is available for blocked CSP requests"); + + const requestEl = document.querySelector( + `.requests-list-column[title*="${scriptFileName}"]` + ).parentNode; + + const waitForHeadersPanel = waitUntil(() => + document.querySelector("#headers-panel .panel-container") + ); + clickElement(requestEl, monitor); + await waitForHeadersPanel; + + ok( + document.querySelector(".headers-overview"), + "There is request overview details" + ); + ok( + document.querySelector(".accordion #requestHeaders"), + "There is request header information" + ); + ok( + !document.querySelector(".accordion #responseHeaders"), + "There is no response header information" + ); + + await teardown(monitor); +} diff --git a/devtools/client/netmonitor/test/browser_net_block-draganddrop.js b/devtools/client/netmonitor/test/browser_net_block-draganddrop.js new file mode 100644 index 0000000000..ff40fee003 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_block-draganddrop.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test blocking and unblocking a request. + */ + +add_task(async function () { + class DataTransfer { + constructor() { + this.BLOCKING_URL = + "https://example.com/browser/devtools/client/netmonitor/test/html_simple-test-page.html"; + this.getDataTrigger = false; + this.setDataTrigger = false; + this.data = ""; + } + + setData(format, data) { + this.setDataTrigger = true; + Assert.strictEqual( + format, + "text/plain", + 'setData passed valid "text/plain" format' + ); + Assert.strictEqual( + data, + this.BLOCKING_URL, + "Data matches the expected URL" + ); + this.data = data; + } + + getData(format) { + this.getDataTrigger = true; + Assert.strictEqual( + format, + "text/plain", + 'getData passed valid "text/plain" format' + ); + return this.data; + } + + wasGetDataTriggered() { + return this.getDataTrigger; + } + + wasSetDataTriggered() { + return this.setDataTrigger; + } + } + + const dataTransfer = new DataTransfer(); + + const { tab, monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Open the request blocking panel + store.dispatch(Actions.toggleRequestBlockingPanel()); + + // Reload to have one request in the list + let waitForEvents = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await waitForEvents; + + // Capture normal request + let normalRequestState; + let normalRequestSize; + { + const requestBlockingPanel = document.querySelector( + ".request-blocking-panel" + ); + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + await waitForHeaders; + normalRequestState = getSelectedRequest(store.getState()); + normalRequestSize = firstRequest.querySelector( + ".requests-list-transferred" + ).textContent; + info("Captured normal request"); + + // Drag and drop the list item + const createBubbledEvent = (type, props = {}) => { + const event = new Event(type, { bubbles: true }); + Object.assign(event, props); + return event; + }; + + info('Dispatching "dragstart" event on first item of request list'); + firstRequest.dispatchEvent( + createBubbledEvent("dragstart", { + clientX: 0, + clientY: 0, + dataTransfer, + }) + ); + + Assert.strictEqual( + dataTransfer.wasSetDataTriggered(), + true, + 'setData() was called during the "dragstart" event' + ); + + info('Dispatching "drop" event on request blocking list'); + requestBlockingPanel.dispatchEvent( + createBubbledEvent("drop", { + clientX: 0, + clientY: 1, + dataTransfer, + }) + ); + + Assert.strictEqual( + dataTransfer.wasGetDataTriggered(), + true, + 'getData() was called during the "drop" event' + ); + + const onRequestBlocked = waitForDispatch( + store, + "REQUEST_BLOCKING_UPDATE_COMPLETE" + ); + + info("Wait for dropped request to be blocked"); + await onRequestBlocked; + info("Dropped request is now blocked"); + } + + // Reload to have one request in the list + info("Reloading to check block"); + // We can't use the normal waiting methods because a canceled request won't send all + // the extra update packets. + waitForEvents = waitForNetworkEvents(monitor, 1); + tab.linkedBrowser.reload(); + await waitForEvents; + + // Capture blocked request, then unblock + let blockedRequestState; + let blockedRequestSize; + { + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + blockedRequestSize = firstRequest.querySelector( + ".requests-list-transferred" + ).textContent; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + blockedRequestState = getSelectedRequest(store.getState()); + info("Captured blocked request"); + } + + ok(!normalRequestState.blockedReason, "Normal request is not blocked"); + ok(!normalRequestSize.includes("Blocked"), "Normal request has a size"); + + ok(blockedRequestState.blockedReason, "Blocked request is blocked"); + ok( + blockedRequestSize.includes("Blocked"), + "Blocked request shows reason as size" + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_block-extensions.js b/devtools/client/netmonitor/test/browser_net_block-extensions.js new file mode 100644 index 0000000000..067f18d3c6 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_block-extensions.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the requests that are blocked by extenstions show correctly. + */ +add_task(async function () { + const extensionName = "Test Blocker"; + info(`Start loading the ${extensionName} extension...`); + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: extensionName, + permissions: ["*://example.com/", "webRequest", "webRequestBlocking"], + }, + useAddonManager: "temporary", + background() { + // eslint-disable-next-line no-undef + browser.webRequest.onBeforeRequest.addListener( + () => { + return { + cancel: true, + }; + }, + { + urls: [ + "https://example.com/browser/devtools/client/netmonitor/test/request_0", + ], + }, + ["blocking"] + ); + // eslint-disable-next-line no-undef + browser.test.sendMessage("ready"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + + const { tab, monitor } = await initNetMonitor(HTTPS_SINGLE_GET_URL, { + requestCount: 2, + }); + + const { document } = monitor.panelWin; + + info("Starting test... "); + + const waitForNetworkEventsToComplete = waitForNetworkEvents(monitor, 2); + const waitForRequestsToRender = waitForDOM( + document, + ".requests-list-row-group" + ); + const waitForReload = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await reloadBrowser(); + + await Promise.all([ + waitForNetworkEventsToComplete, + waitForRequestsToRender, + waitForReload, + ]); + + // Find the request list item that matches the blocked url + const request = document.querySelector( + "td.requests-list-url[title*=request_0]" + ).parentNode; + + info("Assert the blocked request"); + ok( + !!request.querySelector(".requests-list-status .status-code-blocked-icon"), + "The request blocked status icon is visible" + ); + + is( + request.querySelector(".requests-list-status .requests-list-status-code") + .title, + "Blocked", + "The request status title is 'Blocked'" + ); + + is( + request.querySelector(".requests-list-type").innerText, + "", + "The request shows no type" + ); + + is( + request.querySelector(".requests-list-transferred").innerText, + `Blocked By ${extensionName}`, + "The request shows the blocking extension name" + ); + + is( + request.querySelector(".requests-list-size").innerText, + "", + "The request shows no size" + ); + + await teardown(monitor); + info(`Unloading the ${extensionName} extension`); + await extension.unload(); +}); diff --git a/devtools/client/netmonitor/test/browser_net_block-pattern.js b/devtools/client/netmonitor/test/browser_net_block-pattern.js new file mode 100644 index 0000000000..fe4962d777 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_block-pattern.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test basic request blocking functionality for patterns + * Ensures that request blocking unblocks a relevant pattern and not just + * an exact URL match + */ + +add_task(async function () { + await pushPref("devtools.netmonitor.features.requestBlocking", true); + + const { tab, monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Open the request blocking panel + store.dispatch(Actions.toggleRequestBlockingPanel()); + + // Add patterns which should block some of the requests + await addBlockedRequest("test1", monitor); + await addBlockedRequest("test/*/test3", monitor); + + // Close the blocking panel to ensure it's opened by the context menu later + store.dispatch(Actions.toggleRequestBlockingPanel()); + + // Execute two XHRs (the same URL) and wait till they're finished + const TEST_URL_1 = HTTPS_SEARCH_SJS + "?value=test1"; + const TEST_URL_2 = HTTPS_SEARCH_SJS + "?value=test2"; + const TEST_URL_3 = HTTPS_SEARCH_SJS + "test/something/test3"; + const TEST_URL_4 = HTTPS_SEARCH_SJS + "test/something/test4"; + + let wait = waitForNetworkEvents(monitor, 4); + await ContentTask.spawn(tab.linkedBrowser, TEST_URL_1, async function (url) { + content.wrappedJSObject.performRequests(1, url); + }); + await ContentTask.spawn(tab.linkedBrowser, TEST_URL_2, async function (url) { + content.wrappedJSObject.performRequests(1, url); + }); + await ContentTask.spawn(tab.linkedBrowser, TEST_URL_3, async function (url) { + content.wrappedJSObject.performRequests(1, url); + }); + await ContentTask.spawn(tab.linkedBrowser, TEST_URL_4, async function (url) { + content.wrappedJSObject.performRequests(1, url); + }); + await wait; + + // Wait till there are four resources rendered in the results + await waitForDOMIfNeeded(document, ".request-list-item", 4); + + let requestItems = document.querySelectorAll(".request-list-item"); + // Ensure that test1 item was blocked and test2 item wasn't + ok( + checkRequestListItemBlocked(requestItems[0]), + "The first request was blocked" + ); + ok( + !checkRequestListItemBlocked(requestItems[1]), + "The second request was not blocked" + ); + // Ensure that test3 item was blocked and test4 item wasn't + ok( + checkRequestListItemBlocked(requestItems[2]), + "The third request was blocked" + ); + ok( + !checkRequestListItemBlocked(requestItems[3]), + "The fourth request was not blocked" + ); + + EventUtils.sendMouseEvent({ type: "mousedown" }, requestItems[0]); + // Right-click test1, select "Unblock URL" from its context menu + await toggleBlockedUrl(requestItems[0], monitor, store, "unblock"); + + // Ensure that the request blocking panel is now open and the item is unchecked + ok( + !document.querySelector(".request-blocking-list .devtools-checkbox") + .checked, + "The blocking pattern is disabled from context menu" + ); + + // Request the unblocked URL again, ensure the URL was not blocked + wait = waitForNetworkEvents(monitor, 1); + await ContentTask.spawn(tab.linkedBrowser, TEST_URL_1, async function (url) { + content.wrappedJSObject.performRequests(1, url); + }); + await wait; + + await waitForDOMIfNeeded(document, ".request-list-item", 5); + requestItems = document.querySelectorAll(".request-list-item"); + ok( + !checkRequestListItemBlocked(requestItems[4]), + "The fifth request was not blocked" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_block-serviceworker.js b/devtools/client/netmonitor/test/browser_net_block-serviceworker.js new file mode 100644 index 0000000000..03054ce2c5 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_block-serviceworker.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Service workers only work on https +const URL = EXAMPLE_URL.replace("http:", "https:"); + +const TEST_URL = URL + "service-workers/status-codes.html"; + +// Test that request blocking works for service worker requests. +add_task(async function () { + const { tab, monitor } = await initNetMonitor(TEST_URL, { + enableCache: true, + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Registering the service worker..."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.registerServiceWorker(); + }); + + info("Performing the service worker request"); + await performRequests(monitor, tab, 1); + + info("Open the request blocking panel and block service-workers request"); + store.dispatch(Actions.toggleRequestBlockingPanel()); + + info("Block the image request from the service worker"); + await addBlockedRequest("service-workers/test-image.png", monitor); + + await clearNetworkEvents(monitor); + + info("Performing the service worker request again"); + await performRequests(monitor, tab, 1); + + // Wait till there are four resources rendered in the results + await waitForDOMIfNeeded(document, ".request-list-item", 4); + + const requestItems = document.querySelectorAll(".request-list-item"); + ok( + !checkRequestListItemBlocked(requestItems[0]), + "The first service worker request was not blocked" + ); + ok( + checkRequestListItemBlocked(requestItems[requestItems.length - 1]), + "The last service worker request was blocked" + ); + + info("Unregistering the service worker..."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.unregisterServiceWorker(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_block.js b/devtools/client/netmonitor/test/browser_net_block.js new file mode 100644 index 0000000000..c2e7f64f92 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_block.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test blocking and unblocking a request. + */ + +add_task(async function () { + const { monitor, tab } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Reload to have one request in the list + let waitForEvents = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await waitForEvents; + + // Capture normal request + let normalRequestState; + let normalRequestSize; + let normalHeadersSectionSize; + let normalFirstHeaderSectionTitle; + { + // Wait for the response and request header sections + const waitForHeaderSections = waitForDOM( + document, + "#headers-panel .accordion-item", + 2 + ); + + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + + await waitForHeaderSections; + + const headerSections = document.querySelectorAll( + "#headers-panel .accordion-item" + ); + normalRequestState = getSelectedRequest(store.getState()); + normalRequestSize = firstRequest.querySelector( + ".requests-list-transferred" + ).textContent; + normalHeadersSectionSize = headerSections.length; + normalFirstHeaderSectionTitle = headerSections[0].querySelector( + ".accordion-header-label" + ).textContent; + + info("Captured normal request"); + + // Mark as blocked + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest); + const onRequestBlocked = waitForDispatch( + store, + "REQUEST_BLOCKING_UPDATE_COMPLETE" + ); + + await selectContextMenuItem(monitor, "request-list-context-block-url"); + + info("Wait for selected request to be blocked"); + await onRequestBlocked; + info("Selected request is now blocked"); + } + + // Reload to have one request in the list + info("Reloading to check block"); + // We can't use the normal waiting methods because a canceled request won't send all + // the extra update packets. + waitForEvents = waitForNetworkEvents(monitor, 1); + tab.linkedBrowser.reload(); + await waitForEvents; + + // Capture blocked request, then unblock + let blockedRequestState; + let blockedRequestSize; + let blockedHeadersSectionSize; + let blockedFirstHeaderSectionTitle; + { + const waitForHeaderSections = waitForDOM( + document, + "#headers-panel .accordion-item", + 1 + ); + + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + + await waitForHeaderSections; + + const headerSections = document.querySelectorAll( + "#headers-panel .accordion-item" + ); + blockedRequestSize = firstRequest.querySelector( + ".requests-list-transferred" + ).textContent; + blockedRequestState = getSelectedRequest(store.getState()); + blockedHeadersSectionSize = headerSections.length; + blockedFirstHeaderSectionTitle = headerSections[0].querySelector( + ".accordion-header-label" + ).textContent; + + info("Captured blocked request"); + + // Mark as unblocked + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest); + const onRequestUnblocked = waitForDispatch( + store, + "REQUEST_BLOCKING_UPDATE_COMPLETE" + ); + + await selectContextMenuItem(monitor, "request-list-context-unblock-url"); + + info("Wait for selected request to be unblocked"); + await onRequestUnblocked; + info("Selected request is now unblocked"); + } + + // Reload to have one request in the list + info("Reloading to check unblock"); + waitForEvents = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await waitForEvents; + + // Capture unblocked request + let unblockedRequestState; + let unblockedRequestSize; + { + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + unblockedRequestSize = firstRequest.querySelector( + ".requests-list-transferred" + ).textContent; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + unblockedRequestState = getSelectedRequest(store.getState()); + info("Captured unblocked request"); + } + + ok(!normalRequestState.blockedReason, "Normal request is not blocked"); + ok(!normalRequestSize.includes("Blocked"), "Normal request has a size"); + Assert.equal(normalHeadersSectionSize, 2, "Both header sections are showing"); + ok( + normalFirstHeaderSectionTitle.includes("Response"), + "The response header section is visible for normal requests" + ); + + ok(blockedRequestState.blockedReason, "Blocked request is blocked"); + ok( + blockedRequestSize.includes("Blocked"), + "Blocked request shows reason as size" + ); + Assert.equal( + blockedHeadersSectionSize, + 1, + "Only one header section is showing" + ); + ok( + blockedFirstHeaderSectionTitle.includes("Request"), + "The response header section is not visible for blocked requests" + ); + + ok(!unblockedRequestState.blockedReason, "Unblocked request is not blocked"); + ok(!unblockedRequestSize.includes("Blocked"), "Unblocked request has a size"); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_brotli.js b/devtools/client/netmonitor/test/browser_net_brotli.js new file mode 100644 index 0000000000..84576ceca6 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_brotli.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BROTLI_URL = HTTPS_EXAMPLE_URL + "html_brotli-test-page.html"; +const BROTLI_REQUESTS = 1; + +/** + * Test brotli encoded response is handled correctly on HTTPS. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(BROTLI_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, BROTLI_REQUESTS); + + const requestItem = document.querySelector(".request-list-item"); + // Status code title is generated on hover + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + HTTPS_CONTENT_TYPE_SJS + "?fmt=br", + { + status: 200, + statusText: "OK", + type: "json", + fullMimeType: "text/json", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 252), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 64), + time: true, + } + ); + + const wait = waitForDOM(document, ".CodeMirror-code"); + const onResponseContent = monitor.panelWin.api.once( + TEST_EVENTS.RECEIVED_RESPONSE_CONTENT + ); + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await wait; + await onResponseContent; + await testResponse("br"); + await teardown(monitor); + + function testResponse(type) { + switch (type) { + case "br": { + is( + getCodeMirrorValue(monitor), + "X".repeat(64), + "The text shown in the source editor is incorrect for the brotli request." + ); + break; + } + } + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_cache_details.js b/devtools/client/netmonitor/test/browser_net_cache_details.js new file mode 100644 index 0000000000..f15ad6a90b --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_cache_details.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const CACHE_TEST_URL = EXAMPLE_URL + "html_cache-test-page.html"; + +// Test the cache details panel. +add_task(async function () { + const { monitor } = await initNetMonitor(CACHE_TEST_URL, { + enableCache: true, + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + info("Create a 200 request"); + let waitForRequest = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.sendRequestWithStatus("200"); + }); + await waitForRequest; + + info("Select the request and wait until the headers panel is displayed"); + store.dispatch(Actions.selectRequestByIndex(0)); + await waitFor(() => document.querySelector(".headers-overview")); + ok( + !document.querySelector("#cache-tab"), + "No cache panel is available for the 200 request" + ); + + info("Create a 304 request"); + waitForRequest = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.wrappedJSObject.sendRequestWithStatus("304"); + }); + await waitForRequest; + + info("Select the request and wait until the headers panel is displayed"); + store.dispatch(Actions.selectRequestByIndex(1)); + await waitFor(() => document.querySelector(".headers-overview")); + ok( + document.querySelector("#cache-tab"), + "A cache panel is available for the 304 request" + ); + document.querySelector("#cache-tab").click(); + + info("Wait until the Cache panel content is displayed"); + await waitFor(() => !!document.getElementById("/Cache")); + + const device = getCacheDetailsValue(document, "Device"); + is(device, "Not Available", "device information is `Not Available`"); + + // We cannot precisely assert the dates rendered by the cache panel because + // they are formatted using toLocaleDateString/toLocaleTimeString, and + // `new Date` might be unable to parse them. See Bug 1800448. + + // For "last modified" should be the same day as the test, and we could assert + // that. However the cache panel is intermittently fully "Not available", + // except for the "Expires" field, which seems to always have a value. + const lastModified = getCacheDetailsValue(document, "Last Modified"); + info("Retrieved lastModified value: " + lastModified); + ok(!!lastModified, "Last Modified was found in the cache panel"); + + // For "expires" we will only check that this is not set to `Not Available`. + const expires = getCacheDetailsValue(document, "Expires"); + info("Retrieved expires value: " + expires); + ok( + !expires.includes("Not Available"), + "Expires is set to a value other than unavailable" + ); +}); + +/** + * Helper to retrieve individual values from the Cache details panel. + * Eg, for `Expires: "11/9/2022 6:54:33 PM"`, this should return + * "11/9/2022 6:54:33 PM". + * + * @param {Document} doc + * The netmonitor document. + * @param {string} cacheItemId + * The id of the cache element to retrieve. See netmonitor.cache.* localized + * strings. + * + * @returns {string} + * The value corresponding to the provided id. + */ +function getCacheDetailsValue(doc, cacheItemId) { + const container = doc.getElementById("/Cache/" + cacheItemId); + ok(!!container, `Found the cache panel container for id ${cacheItemId}`); + const valueContainer = container.querySelector(".treeValueCell span"); + ok( + !!valueContainer, + `Found the cache panel value container for id ${cacheItemId}` + ); + + // The values have opening and closing quotes, remove them using substring. + // `"some value"` -> `some value` + const quotedValue = valueContainer.textContent; + return quotedValue.substring(1, quotedValue.length - 1); +} diff --git a/devtools/client/netmonitor/test/browser_net_cached-status.js b/devtools/client/netmonitor/test/browser_net_cached-status.js new file mode 100644 index 0000000000..77346e6842 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_cached-status.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if cached requests have the correct status code + */ + +add_task(async function () { + // Disable rcwn to make cache behavior deterministic. + await pushPref("network.http.rcwn.enabled", false); + + const { tab, monitor } = await initNetMonitor(STATUS_CODES_URL, { + enableCache: true, + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const REQUEST_DATA = [ + { + method: "GET", + uri: STATUS_CODES_SJS + "?sts=ok&cached", + details: { + status: 200, + statusText: "OK", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + }, + }, + { + method: "GET", + uri: STATUS_CODES_SJS + "?sts=redirect&cached", + details: { + status: 301, + statusText: "Moved Permanently", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + { + method: "GET", + uri: "http://example.com/redirected", + details: { + status: 404, + statusText: "Not Found", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + { + method: "GET", + uri: STATUS_CODES_SJS + "?sts=ok&cached", + details: { + status: 200, + statusText: "OK (cached)", + displayedStatus: "cached", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + }, + }, + { + method: "GET", + uri: STATUS_CODES_SJS + "?sts=redirect&cached", + details: { + status: 301, + statusText: "Moved Permanently (cached)", + displayedStatus: "cached", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + { + method: "GET", + uri: "http://example.com/redirected", + details: { + status: 404, + statusText: "Not Found", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + ]; + + // Cancel the 200 cached request, so that the test can also assert + // that the NS_BINDING_ABORTED status is never displayed for cached requests. + const observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(subject, topic, data) { + subject = subject.QueryInterface(Ci.nsIHttpChannel); + if (subject.URI.spec == STATUS_CODES_SJS + "?sts=ok&cached") { + subject.cancel(Cr.NS_BINDING_ABORTED); + Services.obs.removeObserver( + observer, + "http-on-examine-cached-response" + ); + } + }, + }; + Services.obs.addObserver(observer, "http-on-examine-cached-response"); + + info("Performing requests #1..."); + await performRequestsAndWait(); + + info("Performing requests #2..."); + await performRequestsAndWait(); + + let index = 0; + for (const request of REQUEST_DATA) { + const requestItem = document.querySelectorAll(".request-list-item")[index]; + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + info("Verifying request #" + index); + await verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[index], + request.method, + request.uri, + request.details + ); + + index++; + } + + await teardown(monitor); + + async function performRequestsAndWait() { + const wait = waitForNetworkEvents(monitor, 3); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.wrappedJSObject.performCachedRequests(); + }); + await wait; + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_cause_redirect.js b/devtools/client/netmonitor/test/browser_net_cause_redirect.js new file mode 100644 index 0000000000..23bf24611e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_cause_redirect.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if request JS stack is property reported if the request is internally + * redirected without hitting the network (HSTS is one of such cases) + */ + +add_task(async function () { + // This test explicitly checks http->https redirects and should not force https. + await pushPref("dom.security.https_first", false); + + const EXPECTED_REQUESTS = [ + // Request to HTTP URL, redirects to HTTPS + { status: 302 }, + // Serves HTTPS, sets the Strict-Transport-Security header + // This request is the redirection caused by the first one + { status: 200 }, + // Second request to HTTP redirects to HTTPS internally + { status: 200 }, + ]; + + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length); + await performRequests(2, HSTS_SJS); + await wait; + + // Fetch stack-trace data from the backend and wait till + // all packets are received. + const requests = getSortedRequests(store.getState()) + .filter(req => !req.stacktrace) + .map(req => connector.requestData(req.id, "stackTrace")); + + await Promise.all(requests); + + EXPECTED_REQUESTS.forEach(({ status }, i) => { + const item = getSortedRequests(store.getState())[i]; + + is( + parseInt(item.status, 10), + status, + `Request #${i} has the expected status` + ); + + const { stacktrace } = item; + const stackLen = stacktrace ? stacktrace.length : 0; + + ok(stacktrace, `Request #${i} has a stacktrace`); + Assert.greater( + stackLen, + 0, + `Request #${i} has a stacktrace with ${stackLen} items` + ); + }); + + // Send a request to reset the HSTS policy to state before the test + wait = waitForNetworkEvents(monitor, 1); + await performRequests(1, HSTS_SJS + "?reset"); + await wait; + + await teardown(monitor); + + function performRequests(count, url) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [{ count, url }], + async function (args) { + content.wrappedJSObject.performRequests(args.count, args.url); + } + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_cause_source_map.js b/devtools/client/netmonitor/test/browser_net_cause_source_map.js new file mode 100644 index 0000000000..740bad3767 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_cause_source_map.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if request cause is reported correctly when using source maps. + */ + +const CAUSE_FILE_NAME = "html_maps-test-page.html"; +const CAUSE_URL = HTTPS_EXAMPLE_URL + CAUSE_FILE_NAME; + +const N_EXPECTED_REQUESTS = 4; + +add_task(async function () { + // the initNetMonitor function clears the network request list after the + // page is loaded. That's why we first load a bogus page from SIMPLE_URL, + // and only then load the real thing from CAUSE_URL - we want to catch + // all the requests the page is making, not only the XHRs. + // We can't use about:blank here, because initNetMonitor checks that the + // page has actually made at least one request. + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + let waitPromise = waitForNetworkEvents(monitor, N_EXPECTED_REQUESTS); + await navigateTo(CAUSE_URL); + await waitPromise; + + info("Clicking item and waiting for details panel to open"); + waitPromise = waitForDOM(document, ".network-details-bar"); + const xhrRequestItem = document.querySelectorAll(".request-list-item")[3]; + EventUtils.sendMouseEvent({ type: "mousedown" }, xhrRequestItem); + await waitPromise; + + info("Clicking stack tab and waiting for stack panel to open"); + waitPromise = waitForDOM(document, "#stack-trace-panel"); + clickOnSidebarTab(document, "stack-trace"); + await waitPromise; + + info("Waiting for source maps to be applied"); + await waitUntil(() => { + const frames = document.querySelectorAll(".frame-link"); + return ( + frames && + frames.length >= 2 && + frames[0].textContent.includes("xhr_original") && + frames[1].textContent.includes("xhr_original") + ); + }); + + const frames = document.querySelectorAll(".frame-link"); + is(frames.length, 3, "should have 3 stack frames"); + is(frames[0].textContent, `reallydoxhr xhr_original.js:6`); + is(frames[1].textContent, `doxhr xhr_original.js:10`); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_charts-01.js b/devtools/client/netmonitor/test/browser_net_charts-01.js new file mode 100644 index 0000000000..7f901e158f --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_charts-01.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Makes sure Pie Charts have the right internal structure. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + + info("Starting test... "); + + const { document, windowRequire } = monitor.panelWin; + const { Chart } = windowRequire("devtools/client/shared/widgets/Chart"); + + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + const pie = Chart.Pie(document, { + width: 100, + height: 100, + data: [ + { + size: 1, + label: "foo", + }, + { + size: 2, + label: "bar", + }, + { + size: 3, + label: "baz", + }, + ], + }); + + const { node } = pie; + const slicesContainer = node.querySelectorAll(".pie-chart-slice-container"); + const slices = node.querySelectorAll(".pie-chart-slice"); + const labels = node.querySelectorAll(".pie-chart-label"); + + ok( + node.classList.contains("pie-chart-container") && + node.classList.contains("generic-chart-container"), + "A pie chart container was created successfully." + ); + is( + node.getAttribute("aria-label"), + "Pie chart representing the size of each type of request in proportion to each other", + "pie chart container has expected aria-label" + ); + + is( + slicesContainer.length, + 3, + "There should be 3 pie chart slices container created." + ); + + is(slices.length, 3, "There should be 3 pie chart slices created."); + ok( + slices[0] + .getAttribute("d") + .match( + /\s*M 50,50 L 49\.\d+,97\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,2\.5\d* Z/ + ), + "The first slice has the correct data." + ); + ok( + slices[1] + .getAttribute("d") + .match( + /\s*M 50,50 L 91\.\d+,26\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,97\.\d+ Z/ + ), + "The second slice has the correct data." + ); + ok( + slices[2] + .getAttribute("d") + .match( + /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 0 1 91\.\d+,26\.\d+ Z/ + ), + "The third slice has the correct data." + ); + + is( + slicesContainer[0].getAttribute("aria-label"), + "baz: 50%", + "First slice container has expected aria-label" + ); + is( + slicesContainer[1].getAttribute("aria-label"), + "bar: 33.33%", + "Second slice container has expected aria-label" + ); + is( + slicesContainer[2].getAttribute("aria-label"), + "foo: 16.67%", + "Third slice container has expected aria-label" + ); + + ok( + slices[0].hasAttribute("largest"), + "The first slice should be the largest one." + ); + ok( + slices[2].hasAttribute("smallest"), + "The third slice should be the smallest one." + ); + + is( + slices[0].getAttribute("data-statistic-name"), + "baz", + "The first slice's name is correct." + ); + is( + slices[1].getAttribute("data-statistic-name"), + "bar", + "The first slice's name is correct." + ); + is( + slices[2].getAttribute("data-statistic-name"), + "foo", + "The first slice's name is correct." + ); + + is(labels.length, 3, "There should be 3 pie chart labels created."); + is(labels[0].textContent, "baz", "The first label's text is correct."); + is(labels[1].textContent, "bar", "The first label's text is correct."); + is(labels[2].textContent, "foo", "The first label's text is correct."); + is( + labels[0].getAttribute("aria-hidden"), + "true", + "The first label has aria-hidden." + ); + is( + labels[1].getAttribute("aria-hidden"), + "true", + "The first label has aria-hidden." + ); + is( + labels[2].getAttribute("aria-hidden"), + "true", + "The first label has aria-hidden." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_charts-02.js b/devtools/client/netmonitor/test/browser_net_charts-02.js new file mode 100644 index 0000000000..106ec88380 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_charts-02.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Makes sure Pie Charts have the right internal structure when + * initialized with empty data. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, windowRequire } = monitor.panelWin; + const { Chart } = windowRequire("devtools/client/shared/widgets/Chart"); + + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + const pie = Chart.Pie(document, { + data: null, + width: 100, + height: 100, + }); + + const { node } = pie; + const slices = node.querySelectorAll(".pie-chart-slice"); + const labels = node.querySelectorAll(".pie-chart-label"); + + ok( + node.classList.contains("pie-chart-container") && + node.classList.contains("generic-chart-container"), + "A pie chart container was created successfully." + ); + + is(slices.length, 1, "There should be 1 pie chart slice created."); + ok( + slices[0] + .getAttribute("d") + .match( + /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/ + ), + "The first slice has the correct data." + ); + + ok( + slices[0].hasAttribute("largest"), + "The first slice should be the largest one." + ); + ok( + slices[0].hasAttribute("smallest"), + "The first slice should also be the smallest one." + ); + is( + slices[0].getAttribute("data-statistic-name"), + L10N.getStr("pieChart.loading"), + "The first slice's name is correct." + ); + + is(labels.length, 1, "There should be 1 pie chart label created."); + is(labels[0].textContent, "Loading", "The first label's text is correct."); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_charts-03.js b/devtools/client/netmonitor/test/browser_net_charts-03.js new file mode 100644 index 0000000000..f938bb3932 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_charts-03.js @@ -0,0 +1,195 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Makes sure Table Charts have the right internal structure. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, windowRequire } = monitor.panelWin; + const { Chart } = windowRequire("devtools/client/shared/widgets/Chart"); + + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + const table = Chart.Table(document, { + title: "Table title", + data: [ + { + label1: 1, + label2: 11.1, + }, + { + label1: 2, + label2: 12.2, + }, + { + label1: 3, + label2: 13.3, + }, + ], + strings: { + label2: (value, index) => value + ["foo", "bar", "baz"][index], + }, + totals: { + label1: value => "Hello " + L10N.numberWithDecimals(value, 2), + label2: value => "World " + L10N.numberWithDecimals(value, 2), + }, + header: { + label1: "label1header", + label2: "label2header", + }, + }); + + const { node } = table; + const title = node.querySelector(".table-chart-title"); + const grid = node.querySelector(".table-chart-grid"); + const totals = node.querySelector(".table-chart-totals"); + const rows = grid.querySelectorAll(".table-chart-row"); + const sums = node.querySelectorAll(".table-chart-summary-label"); + + ok( + node.classList.contains("table-chart-container") && + node.classList.contains("generic-chart-container"), + "A table chart container was created successfully." + ); + + ok(title, "A title node was created successfully."); + is( + title.textContent, + "Table title", + "The title node displays the correct text." + ); + + is( + rows.length, + 4, + "There should be 3 table chart rows and a header created." + ); + + is( + rows[0].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"), + "label1", + "The first column of the header exists." + ); + is( + rows[0].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"), + "label2", + "The second column of the header exists." + ); + is( + rows[0].querySelectorAll(".table-chart-row-label")[0].textContent, + "label1header", + "The first column of the header displays the correct text." + ); + is( + rows[0].querySelectorAll(".table-chart-row-label")[1].textContent, + "label2header", + "The second column of the header displays the correct text." + ); + + is( + rows[1].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"), + "label1", + "The first column of the first row exists." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"), + "label2", + "The second column of the first row exists." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[0].textContent, + "1", + "The first column of the first row displays the correct text." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[1].textContent, + "11.1foo", + "The second column of the first row displays the correct text." + ); + + is( + rows[2].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"), + "label1", + "The first column of the second row exists." + ); + is( + rows[2].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"), + "label2", + "The second column of the second row exists." + ); + is( + rows[2].querySelectorAll(".table-chart-row-label")[0].textContent, + "2", + "The first column of the second row displays the correct text." + ); + is( + rows[2].querySelectorAll(".table-chart-row-label")[1].textContent, + "12.2bar", + "The second column of the first row displays the correct text." + ); + + is( + rows[3].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"), + "label1", + "The first column of the third row exists." + ); + is( + rows[3].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"), + "label2", + "The second column of the third row exists." + ); + is( + rows[3].querySelectorAll(".table-chart-row-label")[0].textContent, + "3", + "The first column of the third row displays the correct text." + ); + is( + rows[3].querySelectorAll(".table-chart-row-label")[1].textContent, + "13.3baz", + "The second column of the third row displays the correct text." + ); + + is(sums.length, 2, "There should be 2 total summaries created."); + + is( + totals + .querySelectorAll(".table-chart-summary-label")[0] + .getAttribute("name"), + "label1", + "The first sum's type is correct." + ); + is( + totals.querySelectorAll(".table-chart-summary-label")[0].textContent, + "Hello 6", + "The first sum's value is correct." + ); + + is( + totals + .querySelectorAll(".table-chart-summary-label")[1] + .getAttribute("name"), + "label2", + "The second sum's type is correct." + ); + is( + totals.querySelectorAll(".table-chart-summary-label")[1].textContent, + "World 36.60", + "The second sum's value is correct." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_charts-04.js b/devtools/client/netmonitor/test/browser_net_charts-04.js new file mode 100644 index 0000000000..686608fb81 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_charts-04.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Makes sure Pie Charts have the right internal structure when + * initialized with empty data. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, windowRequire } = monitor.panelWin; + const { Chart } = windowRequire("devtools/client/shared/widgets/Chart"); + + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + const table = Chart.Table(document, { + title: "Table title", + data: null, + totals: { + label1: value => "Hello " + L10N.numberWithDecimals(value, 2), + label2: value => "World " + L10N.numberWithDecimals(value, 2), + }, + header: { + label1: "", + label2: "", + }, + }); + + const { node } = table; + const title = node.querySelector(".table-chart-title"); + const grid = node.querySelector(".table-chart-grid"); + const totals = node.querySelector(".table-chart-totals"); + const rows = grid.querySelectorAll(".table-chart-row"); + const sums = node.querySelectorAll(".table-chart-summary-label"); + + ok( + node.classList.contains("table-chart-container") && + node.classList.contains("generic-chart-container"), + "A table chart container was created successfully." + ); + + ok(title, "A title node was created successfully."); + is( + title.textContent, + "Table title", + "The title node displays the correct text." + ); + + is( + rows.length, + 2, + "There should be 1 table chart row and a 1 header created." + ); + + is( + rows[1].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"), + "size", + "The first column of the first row exists." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"), + "label", + "The second column of the first row exists." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[0].textContent, + "", + "The first column of the first row displays the correct text." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[1].textContent, + L10N.getStr("tableChart.loading"), + "The second column of the first row displays the correct text." + ); + + is(sums.length, 2, "There should be 2 total summaries created."); + + is( + totals + .querySelectorAll(".table-chart-summary-label")[0] + .getAttribute("name"), + "label1", + "The first sum's type is correct." + ); + is( + totals.querySelectorAll(".table-chart-summary-label")[0].textContent, + "Hello 0", + "The first sum's value is correct." + ); + + is( + totals + .querySelectorAll(".table-chart-summary-label")[1] + .getAttribute("name"), + "label2", + "The second sum's type is correct." + ); + is( + totals.querySelectorAll(".table-chart-summary-label")[1].textContent, + "World 0", + "The second sum's value is correct." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_charts-05.js b/devtools/client/netmonitor/test/browser_net_charts-05.js new file mode 100644 index 0000000000..5a75eee610 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_charts-05.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Makes sure Pie+Table Charts have the right internal structure. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, windowRequire } = monitor.panelWin; + const { Chart } = windowRequire("devtools/client/shared/widgets/Chart"); + + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + const chart = Chart.PieTable(document, { + title: "Table title", + data: [ + { + size: 1, + label: 11.1, + }, + { + size: 2, + label: 12.2, + }, + { + size: 3, + label: 13.3, + }, + ], + strings: { + label2: (value, index) => value + ["foo", "bar", "baz"][index], + }, + totals: { + size: value => "Hello " + L10N.numberWithDecimals(value, 2), + label: value => "World " + L10N.numberWithDecimals(value, 2), + }, + header: { + label1: "", + label2: "", + }, + }); + + ok(chart.pie, "The pie chart proxy is accessible."); + ok(chart.table, "The table chart proxy is accessible."); + + const { node } = chart; + const rows = node.querySelectorAll(".table-chart-row"); + const sums = node.querySelectorAll(".table-chart-summary-label"); + + ok( + node.classList.contains("pie-table-chart-container"), + "A pie+table chart container was created successfully." + ); + + ok( + node.querySelector(".table-chart-title"), + "A title node was created successfully." + ); + ok( + node.querySelector(".pie-chart-container"), + "A pie chart was created successfully." + ); + ok( + node.querySelector(".table-chart-container"), + "A table chart was created successfully." + ); + + is( + rows.length, + 4, + "There should be 3 pie chart slices and 1 header created." + ); + is( + rows.length, + 4, + "There should be 3 table chart rows and 1 header created." + ); + is(sums.length, 2, "There should be 2 total summaries and 1 header created."); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_charts-06.js b/devtools/client/netmonitor/test/browser_net_charts-06.js new file mode 100644 index 0000000000..a41e997873 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_charts-06.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Makes sure Pie Charts correctly handle empty source data. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, windowRequire } = monitor.panelWin; + const { Chart } = windowRequire("devtools/client/shared/widgets/Chart"); + + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + const pie = Chart.Pie(document, { + data: [], + width: 100, + height: 100, + }); + + const { node } = pie; + const slices = node.querySelectorAll(".pie-chart-slice"); + const labels = node.querySelectorAll(".pie-chart-label"); + + is(slices.length, 1, "There should be 1 pie chart slice created."); + ok( + slices[0] + .getAttribute("d") + .match( + /\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/ + ), + "The slice has the correct data." + ); + + ok(slices[0].hasAttribute("largest"), "The slice should be the largest one."); + ok( + slices[0].hasAttribute("smallest"), + "The slice should also be the smallest one." + ); + is( + slices[0].getAttribute("data-statistic-name"), + L10N.getStr("pieChart.unavailable"), + "The slice's name is correct." + ); + + is(labels.length, 1, "There should be 1 pie chart label created."); + is(labels[0].textContent, "Empty", "The label's text is correct."); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_charts-07.js b/devtools/client/netmonitor/test/browser_net_charts-07.js new file mode 100644 index 0000000000..9744c33e6d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_charts-07.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Makes sure Table Charts correctly handle empty source data. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, windowRequire } = monitor.panelWin; + const { Chart } = windowRequire("devtools/client/shared/widgets/Chart"); + + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + const table = Chart.Table(document, { + data: [], + totals: { + label1: value => "Hello " + L10N.numberWithDecimals(value, 2), + label2: value => "World " + L10N.numberWithDecimals(value, 2), + }, + header: { + label1: "", + label2: "", + }, + }); + + const { node } = table; + const grid = node.querySelector(".table-chart-grid"); + const totals = node.querySelector(".table-chart-totals"); + const rows = grid.querySelectorAll(".table-chart-row"); + const sums = node.querySelectorAll(".table-chart-summary-label"); + + is(rows.length, 2, "There should be 1 table chart row and 1 header created."); + + is( + rows[1].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"), + "size", + "The first column of the first row exists." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"), + "label", + "The second column of the first row exists." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[0].textContent, + "", + "The first column of the first row displays the correct text." + ); + is( + rows[1].querySelectorAll(".table-chart-row-label")[1].textContent, + L10N.getStr("tableChart.unavailable"), + "The second column of the first row displays the correct text." + ); + + is(sums.length, 2, "There should be 2 total summaries created."); + + is( + totals + .querySelectorAll(".table-chart-summary-label")[0] + .getAttribute("name"), + "label1", + "The first sum's type is correct." + ); + is( + totals.querySelectorAll(".table-chart-summary-label")[0].textContent, + "Hello 0", + "The first sum's value is correct." + ); + + is( + totals + .querySelectorAll(".table-chart-summary-label")[1] + .getAttribute("name"), + "label2", + "The second sum's type is correct." + ); + is( + totals.querySelectorAll(".table-chart-summary-label")[1].textContent, + "World 0", + "The second sum's value is correct." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_clear.js b/devtools/client/netmonitor/test/browser_net_clear.js new file mode 100644 index 0000000000..5a97de3e02 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_clear.js @@ -0,0 +1,145 @@ +/* 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/webconsole/test/browser/shared-head.js", + this +); + +/** + * Tests if the clear button empties the request menu. + */ + +add_task(async function () { + Services.prefs.setBoolPref("devtools.webconsole.filter.net", true); + + const { monitor, toolbox } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const clearButton = document.querySelector(".requests-list-clear-button"); + + store.dispatch(Actions.batchEnable(false)); + + // Make sure we start in a sane state + assertNoRequestState(); + + // Load one request and assert it shows up in the list + let wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + assertSingleRequestState(); + assertNetworkEventResourceState(1); + + info("Swith to the webconsole and wait for network logs"); + const onWebConsole = monitor.toolbox.once("webconsole-selected"); + const { hud } = await monitor.toolbox.selectTool("webconsole"); + await onWebConsole; + + info("Wait for request"); + await waitFor(() => findMessageByType(hud, SIMPLE_URL, ".network")); + + info("Switch back the the netmonitor"); + await monitor.toolbox.selectTool("netmonitor"); + + // Click clear and make sure the requests are gone + let waitRequestListCleared = waitForEmptyRequestList(document); + EventUtils.sendMouseEvent({ type: "click" }, clearButton); + await waitRequestListCleared; + + assertNoRequestState(); + assertNetworkEventResourceState(0); + + info( + "Swith back to the webconsole to assert that the cleared request gets disabled" + ); + await monitor.toolbox.selectTool("webconsole"); + + info("Wait for network request to show and that its disabled"); + + await waitFor(() => findMessageByType(hud, SIMPLE_URL, ".network.disabled")); + + // Switch back to the netmonitor. + await monitor.toolbox.selectTool("netmonitor"); + + // Load a second request and make sure they still show up + wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + assertSingleRequestState(); + assertNetworkEventResourceState(1); + + // Make sure we can now open the network details panel + store.dispatch(Actions.toggleNetworkDetails()); + const detailsPanelToggleButton = document.querySelector(".sidebar-toggle"); + // Wait for the details panel to finish fetching the headers information + await waitForRequestData(store, ["requestHeaders", "responseHeaders"]); + + ok( + detailsPanelToggleButton && + !detailsPanelToggleButton.classList.contains("pane-collapsed"), + "The details pane should be visible." + ); + + // Click clear and make sure the details pane closes + waitRequestListCleared = waitForEmptyRequestList(document); + EventUtils.sendMouseEvent({ type: "click" }, clearButton); + await waitRequestListCleared; + + assertNoRequestState(); + assertNetworkEventResourceState(0); + + ok( + !document.querySelector(".network-details-bar"), + "The details pane should not be visible clicking 'clear'." + ); + + return teardown(monitor); + + /** + * Asserts the state of the network monitor when one request has loaded + */ + function assertSingleRequestState() { + is( + store.getState().requests.requests.length, + 1, + "The request menu should have one item at this point." + ); + } + + /** + * Asserts the state of the network monitor when no requests have loaded + */ + function assertNoRequestState() { + is( + store.getState().requests.requests.length, + 0, + "The request menu should be empty at this point." + ); + } + + function assertNetworkEventResourceState(expectedNoOfNetworkEventResources) { + const actualNoOfNetworkEventResources = + toolbox.resourceCommand.getAllResources( + toolbox.resourceCommand.TYPES.NETWORK_EVENT + ).length; + + is( + actualNoOfNetworkEventResources, + expectedNoOfNetworkEventResources, + `The expected number of network resources is correctly ${actualNoOfNetworkEventResources}` + ); + } + + function waitForEmptyRequestList(doc) { + info("Wait for request list to clear"); + return waitFor(() => !!doc.querySelector(".request-list-empty-notice")); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_column-resize-fit.js b/devtools/client/netmonitor/test/browser_net_column-resize-fit.js new file mode 100644 index 0000000000..9b3879682e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_column-resize-fit.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests resizing of columns in NetMonitor. + */ +add_task(async function () { + // Reset visibleColumns so we only get the default ones + // and not all that are set in head.js + Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns"); + const visibleColumns = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.visibleColumns") + ); + // Init network monitor + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document } = monitor.panelWin; + + // Wait for network events (to have some requests in the table) + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + info("Testing column resize to fit using double-click on draggable resizer"); + const fileHeader = document.querySelector(`#requests-list-file-header-box`); + const fileColumnResizer = fileHeader.querySelector(".column-resizer"); + + EventUtils.sendMouseEvent({ type: "dblclick" }, fileColumnResizer); + + // After resize - get fresh prefs for tests. + let columnsData = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.columnsData") + ); + + // `File` column before resize: 25%, after resize: 11.25% + // `Transferred` column before resize: 10%, after resize: 10% + checkColumnsData(columnsData, "file", 12); + checkSumOfVisibleColumns(columnsData, visibleColumns); + + info( + "Testing column resize to fit using context menu `Resize Column To Fit Content`" + ); + + // Resizing `transferred` column. + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector("#requests-list-transferred-button") + ); + + await selectContextMenuItem( + monitor, + "request-list-header-resize-column-to-fit-content" + ); + + columnsData = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.columnsData") + ); + + // `Transferred` column before resize: 10%, after resize: 2.97% + checkColumnsData(columnsData, "transferred", 3); + checkSumOfVisibleColumns(columnsData, visibleColumns); + + // Done: clean up. + return teardown(monitor); +}); + +function checkColumnsData(columnsData, column, expectedWidth) { + const width = getWidthFromPref(columnsData, column); + const widthsDiff = Math.abs(width - expectedWidth); + Assert.less( + widthsDiff, + 2, + `Column ${column} has expected size. Got ${width}, Expected ${expectedWidth}` + ); +} + +function checkSumOfVisibleColumns(columnsData, visibleColumns) { + let sum = 0; + visibleColumns.forEach(column => { + sum += getWidthFromPref(columnsData, column); + }); + sum = Math.round(sum); + is(sum, 100, "All visible columns cover 100%."); +} + +function getWidthFromPref(columnsData, column) { + const widthInPref = columnsData.find(function (element) { + return element.name === column; + }).width; + return widthInPref; +} diff --git a/devtools/client/netmonitor/test/browser_net_column_headers_tooltips.js b/devtools/client/netmonitor/test/browser_net_column_headers_tooltips.js new file mode 100644 index 0000000000..56a23c058d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_column_headers_tooltips.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1377094 - Test that all column headers have tooltips. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + info("Starting test... "); + + const { document } = monitor.panelWin; + + const headers = document.querySelectorAll(".requests-list-header-button"); + for (const header of headers) { + const buttonText = header.querySelector(".button-text").textContent; + const tooltip = header.getAttribute("title"); + is( + buttonText, + tooltip, + "The " + + header.id + + " header has the button text in its 'title' attribute." + ); + } + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_column_slow-request-indicator.js b/devtools/client/netmonitor/test/browser_net_column_slow-request-indicator.js new file mode 100644 index 0000000000..368873c3c6 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_column_slow-request-indicator.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the slow request indicator is visible for slow requests. + */ + +add_task(async function () { + // The script sjs_slow-script-server.sjs takes about 2s which is + // definately above the slow threshold set here. + const SLOW_THRESHOLD = 450; + + Services.prefs.setIntPref("devtools.netmonitor.audits.slow", SLOW_THRESHOLD); + + const { monitor } = await initNetMonitor(SLOW_REQUESTS_URL, { + requestCount: 2, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 2); + await reloadBrowser(); + await wait; + + const requestList = document.querySelectorAll( + ".network-monitor .request-list-item" + ); + + info("Checking the html document request"); + is( + requestList[0].querySelector(".requests-list-file div:first-child") + .textContent, + "html_slow-requests-test-page.html", + "The html document is the first request" + ); + is( + !!requestList[0].querySelector(".requests-list-slow-button"), + false, + "The request is not slow" + ); + + info("Checking the slow script request"); + is( + requestList[1].querySelector(".requests-list-file div:first-child") + .textContent, + "sjs_slow-script-server.sjs", + "The slow test script is the second request" + ); + is( + !!requestList[1].querySelector(".requests-list-slow-button"), + true, + "The request is slow" + ); + + is( + requestList[1] + .querySelector(".requests-list-slow-button") + .title.includes(`The recommended limit is ${SLOW_THRESHOLD} ms.`), + true, + "The tooltip text is correct" + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_columns_last_column.js b/devtools/client/netmonitor/test/browser_net_columns_last_column.js new file mode 100644 index 0000000000..aaed0aa210 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_columns_last_column.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that last visible column can't be hidden. Note that the column + * header is visible only if there are requests in the list. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + const initialColumns = store.getState().ui.columns; + for (const column in initialColumns) { + const shown = initialColumns[column]; + + const columns = store.getState().ui.columns; + const visibleColumns = []; + for (const c in columns) { + if (columns[c]) { + visibleColumns.push(c); + } + } + + if (visibleColumns.length === 1) { + if (!shown) { + continue; + } + await testLastMenuItem(column); + break; + } + + if (shown) { + await hideColumn(monitor, column); + } + } + + await teardown(monitor); + + async function testLastMenuItem(column) { + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector(`#requests-list-${column}-button`) + ); + + const menuItem = getContextMenuItem( + monitor, + `request-list-header-${column}-toggle` + ); + ok(menuItem.disabled, "Last visible column menu item should be disabled."); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_columns_pref.js b/devtools/client/netmonitor/test/browser_net_columns_pref.js new file mode 100644 index 0000000000..4c64ac8f94 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_columns_pref.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if visible columns are properly saved. Note that the column + * header is visible only if there are requests in the list. + */ + +add_task(async function () { + Services.prefs.setCharPref( + "devtools.netmonitor.visibleColumns", + '["status", "contentSize", "waterfall"]' + ); + + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + ok( + document.querySelector("#requests-list-status-button"), + "Status column should be shown" + ); + ok( + document.querySelector("#requests-list-contentSize-button"), + "Content size column should be shown" + ); + + await hideColumn(monitor, "status"); + await hideColumn(monitor, "contentSize"); + + let visibleColumns = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.visibleColumns") + ); + + ok(!visibleColumns.includes("status"), "Pref should be synced for status"); + ok( + !visibleColumns.includes("contentSize"), + "Pref should be synced for contentSize" + ); + + await showColumn(monitor, "status"); + + visibleColumns = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.visibleColumns") + ); + + ok(visibleColumns.includes("status"), "Pref should be synced for status"); +}); diff --git a/devtools/client/netmonitor/test/browser_net_columns_reset.js b/devtools/client/netmonitor/test/browser_net_columns_reset.js new file mode 100644 index 0000000000..135c024d44 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_columns_reset.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests reset column menu item. Note that the column + * header is visible only if there are requests in the list. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { Prefs } = windowRequire("devtools/client/netmonitor/src/utils/prefs"); + + const prefBefore = Prefs.visibleColumns; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + await hideColumn(monitor, "status"); + await hideColumn(monitor, "waterfall"); + + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector("#requests-list-contentSize-button") + ); + + await selectContextMenuItem(monitor, "request-list-header-reset-columns"); + + Assert.strictEqual( + JSON.stringify(prefBefore), + JSON.stringify(Prefs.visibleColumns), + "Reset columns item should reset columns pref" + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_columns_showhide.js b/devtools/client/netmonitor/test/browser_net_columns_showhide.js new file mode 100644 index 0000000000..7213d2c5f7 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_columns_showhide.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test showing/hiding columns. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, connector, windowRequire } = monitor.panelWin; + const { requestData } = connector; + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + const wait = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + const item = getSortedRequests(store.getState())[0]; + ok( + item.responseHeadersAvailable, + "headers are available for lazily fetching" + ); + + if (item.responseHeadersAvailable && !item.responseHeaders) { + await requestData(item.id, "responseHeaders"); + } + + const requestsContainer = document.querySelector(".requests-list-row-group"); + ok(requestsContainer, "Container element exists as expected."); + const headers = document.querySelector(".requests-list-headers"); + + let columns = store.getState().ui.columns; + for (const column in columns) { + if (columns[column]) { + await testVisibleColumnContextMenuItem(column, document, monitor); + testColumnsAlignment(headers, requestsContainer); + await testHiddenColumnContextMenuItem(column, document, monitor); + } else { + await testHiddenColumnContextMenuItem(column, document, monitor); + testColumnsAlignment(headers, requestsContainer); + await testVisibleColumnContextMenuItem(column, document, monitor); + } + } + + columns = store.getState().ui.columns; + for (const column in columns) { + if (columns[column]) { + await testVisibleColumnContextMenuItem(column, document, monitor); + // Right click on the white-space for the context menu to appear + // and toggle column visibility + await testWhiteSpaceContextMenuItem(column, document, monitor); + } + } +}); + +async function testWhiteSpaceContextMenuItem(column, document, monitor) { + ok( + !document.querySelector(`#requests-list-${column}-button`), + `Column ${column} should be hidden` + ); + + info(`Right clicking on white-space in the header to get the context menu`); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector(".requests-list-headers") + ); + + // Wait for next tick to do stuff async and force repaint. + await waitForTick(); + await toggleAndCheckColumnVisibility(column, document, monitor); +} + +async function testVisibleColumnContextMenuItem(column, document, monitor) { + ok( + document.querySelector(`#requests-list-${column}-button`), + `Column ${column} should be visible` + ); + + info(`Clicking context-menu item for ${column}`); + + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector("#requests-list-status-button") || + document.querySelector("#requests-list-waterfall-button") + ); + + await waitForTick(); + + const id = `request-list-header-${column}-toggle`; + const menuItem = getContextMenuItem(monitor, id); + + is( + menuItem.getAttribute("type"), + "checkbox", + `${column} menu item should have type="checkbox" attribute` + ); + is( + menuItem.getAttribute("checked"), + "true", + `checked state of ${column} menu item should be correct` + ); + ok( + !menuItem.disabled, + `disabled state of ${column} menu item should be correct` + ); + + const onHeaderRemoved = waitForDOM( + document, + `#requests-list-${column}-button`, + 0 + ); + + await selectContextMenuItem(monitor, id); + + await onHeaderRemoved; + await waitForTick(); + + ok( + !document.querySelector(`#requests-list-${column}-button`), + `Column ${column} should be hidden` + ); +} + +async function testHiddenColumnContextMenuItem(column, document, monitor) { + ok( + !document.querySelector(`#requests-list-${column}-button`), + `Column ${column} should be hidden` + ); + + info(`Clicking context-menu item for ${column}`); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector("#requests-list-status-button") || + document.querySelector("#requests-list-waterfall-button") + ); + + await waitForTick(); + await toggleAndCheckColumnVisibility(column, document, monitor); +} + +async function toggleAndCheckColumnVisibility(column, document, monitor) { + const id = `request-list-header-${column}-toggle`; + const menuItem = getContextMenuItem(monitor, id); + + is( + menuItem.getAttribute("type"), + "checkbox", + `${column} menu item should have type="checkbox" attribute` + ); + ok( + !menuItem.getAttribute("checked"), + `checked state of ${column} menu item should be correct` + ); + ok( + !menuItem.disabled, + `disabled state of ${column} menu item should be correct` + ); + + const onHeaderAdded = waitForDOM( + document, + `#requests-list-${column}-button`, + 1 + ); + + await selectContextMenuItem(monitor, id); + + await onHeaderAdded; + await waitForTick(); + + ok( + document.querySelector(`#requests-list-${column}-button`), + `Column ${column} should be visible` + ); +} diff --git a/devtools/client/netmonitor/test/browser_net_columns_time.js b/devtools/client/netmonitor/test/browser_net_columns_time.js new file mode 100644 index 0000000000..c2e9a9b858 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_columns_time.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for timings columns. Note that the column + * header is visible only if there are requests in the list. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const visibleColumns = store.getState().ui.columns; + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + // Hide the waterfall column to make sure timing data are fetched + // by the other timing columns ("endTime", "responseTime", "duration", + // "latency"). + // Note that all these timing columns are based on the same + // `RequestListColumnTime` component. + if (visibleColumns.waterfall) { + await hideColumn(monitor, "waterfall"); + } + + ["endTime", "responseTime", "duration", "latency"].forEach(async column => { + if (!visibleColumns[column]) { + await showColumn(monitor, column); + } + }); + + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await onNetworkEvents; + + // There should be one request in the list. + const requestItems = document.querySelectorAll(".request-list-item"); + is(requestItems.length, 1, "There must be one visible item"); + + const item = requestItems[0]; + const types = ["end", "response", "duration", "latency"]; + + for (const t of types) { + await waitUntil(() => { + const node = item.querySelector(".requests-list-" + t + "-time"); + const value = parseInt(node.textContent, 10); + return value > 0; + }); + } + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_complex-params.js b/devtools/client/netmonitor/test/browser_net_complex-params.js new file mode 100644 index 0000000000..bde3b31f7b --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_complex-params.js @@ -0,0 +1,314 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether complex request params and payload sent via POST are + * displayed correctly. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(PARAMS_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 12); + + const requestListItems = document.querySelectorAll( + ".network-monitor .request-list-item" + ); + + // Select the Request tab. + EventUtils.sendMouseEvent({ type: "mousedown" }, requestListItems[0]); + clickOnSidebarTab(document, "request"); + + await testRequestWithFormattedView( + monitor, + requestListItems[0], + '{ "foo": "bar" }', + "", + '{ "foo": "bar" }', + 1 + ); + await testRequestWithFormattedView( + monitor, + requestListItems[1], + '{ "foo": "bar" }', + "", + '{ "foo": "bar" }', + 1 + ); + await testRequestWithFormattedView( + monitor, + requestListItems[2], + "?foo", + "bar=123=xyz", + "?foo=bar=123=xyz", + 1 + ); + await testRequestWithFormattedView( + monitor, + requestListItems[3], + "foo", + "bar", + '{ "foo": "bar" }', + 2 + ); + await testRequestWithFormattedView( + monitor, + requestListItems[4], + "foo", + "bar", + '{ "foo": "bar" }', + 2 + ); + await testRequestWithOnlyRawDataView( + monitor, + requestListItems[5], + "?foo=bar" + ); + await testRequestWithoutRequestData(monitor, requestListItems[6]); + await testRequestWithFormattedView( + monitor, + requestListItems[7], + '{ "foo": "bar" }', + "", + '{ "foo": "bar" }', + 1 + ); + await testRequestWithFormattedView( + monitor, + requestListItems[8], + '{ "foo": "bar" }', + "", + '{ "foo": "bar" }', + 1 + ); + + await teardown(monitor); +}); + +async function testRequestWithFormattedView( + monitor, + requestListItem, + paramName, + paramValue, + rawValue, + dataType +) { + const { document, windowRequire } = monitor.panelWin; + const { L10N } = windowRequire("devtools/client/netmonitor/src/utils/l10n"); + + // Wait for header and properties view to be displayed + const wait = waitForDOM(document, "#request-panel .data-header"); + let waitForContent = waitForDOM(document, "#request-panel .properties-view"); + EventUtils.sendMouseEvent({ type: "mousedown" }, requestListItem); + await Promise.all([wait, waitForContent]); + + const tabpanel = document.querySelector("#request-panel"); + let headerLabel; + switch (dataType) { + case 1: + headerLabel = L10N.getStr("paramsFormData"); + break; + + case 2: + headerLabel = L10N.getStr("jsonScopeName"); + break; + } + + is( + tabpanel.querySelectorAll(".raw-data-toggle").length, + 1, + "The raw request data toggle should be displayed in this tabpanel." + ); + is( + tabpanel.querySelectorAll("tr.treeRow").length, + 1, + "The number of param rows displayed in this tabpanel is incorrect." + ); + Assert.strictEqual( + tabpanel.querySelector(".empty-notice"), + null, + "The empty notice should not be displayed in this tabpanel." + ); + + ok( + tabpanel.querySelector(".treeTable"), + "The request params box should be displayed." + ); + Assert.strictEqual( + tabpanel.querySelector(".CodeMirror-code"), + null, + "The request post data editor should not be displayed." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is( + tabpanel.querySelector(".data-label").textContent, + headerLabel, + "The form data section doesn't have the correct title." + ); + + is( + labels[0].textContent, + paramName, + "The first form data param name was incorrect." + ); + is( + values[0].textContent, + `"${paramValue}"`, + "The first form data param value was incorrect." + ); + + // Toggle the raw data display. This should hide the formatted display. + waitForContent = waitForDOM(document, "#request-panel .CodeMirror-code"); + let rawDataToggle = document.querySelector( + "#request-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawDataToggle, monitor); + await waitForContent; + + const dataLabel = tabpanel.querySelector(".data-label") ?? {}; + is( + dataLabel.textContent, + L10N.getStr("paramsPostPayload"), + "The label for the raw request payload is correct." + ); + is( + tabpanel.querySelector(".raw-data-toggle-input .devtools-checkbox-toggle") + .checked, + true, + "The raw request toggle should be on." + ); + is( + tabpanel.querySelector(".properties-view") === null, + true, + "The formatted display should be hidden." + ); + is( + tabpanel.querySelector(".CodeMirror-code") !== null, + true, + "The raw payload data display is shown." + ); + is( + getCodeMirrorValue(monitor), + rawValue, + "The raw payload data string needs to be correct." + ); + Assert.strictEqual( + tabpanel.querySelector(".empty-notice"), + null, + "The empty notice should not be displayed in this tabpanel." + ); + + // Toggle the raw data display off again. This should show the formatted display. + // This is required to reset the original state + waitForContent = waitForDOM(document, "#request-panel .properties-view"); + rawDataToggle = document.querySelector( + "#request-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawDataToggle, monitor); + await waitForContent; +} + +async function testRequestWithOnlyRawDataView( + monitor, + requestListItem, + paramName +) { + const { document, windowRequire } = monitor.panelWin; + const { L10N } = windowRequire("devtools/client/netmonitor/src/utils/l10n"); + + // Wait for header and CodeMirror editor to be displayed + const wait = waitForDOM(document, "#request-panel .data-header"); + const waitForContent = waitForDOM( + document, + "#request-panel .CodeMirror-code" + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, requestListItem); + await Promise.all([wait, waitForContent]); + + const tabpanel = document.querySelector("#request-panel"); + + const dataLabel = tabpanel.querySelector(".data-label") ?? {}; + is( + dataLabel.textContent, + L10N.getStr("paramsPostPayload"), + "The label for the raw request payload is correct." + ); + is( + tabpanel.querySelectorAll(".raw-data-toggle").length, + 0, + "The raw request data toggle should not be displayed in this tabpanel." + ); + is( + tabpanel.querySelector(".properties-view") === null, + true, + "The formatted display should be hidden." + ); + is( + tabpanel.querySelector(".CodeMirror-code") !== null, + true, + "The raw payload data display is shown." + ); + is( + getCodeMirrorValue(monitor), + paramName, + "The raw payload data string needs to be correct." + ); + Assert.strictEqual( + tabpanel.querySelector(".empty-notice"), + null, + "The empty notice should not be displayed in this tabpanel." + ); +} + +async function testRequestWithoutRequestData(monitor, requestListItem) { + const { document } = monitor.panelWin; + + EventUtils.sendMouseEvent({ type: "mousedown" }, requestListItem); + + const tabpanel = document.querySelector("#request-panel"); + + is( + tabpanel.querySelector(".data-label") === null, + true, + "There must be no label for the request payload." + ); + is( + tabpanel.querySelectorAll(".raw-data-toggle").length, + 0, + "The raw request data toggle should not be displayed in this tabpanel." + ); + is( + tabpanel.querySelector(".properties-view") === null, + true, + "The formatted display should be hidden." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + true, + "The raw payload data display should be hidden." + ); + is( + tabpanel.querySelector(".empty-notice") !== null, + true, + "The empty notice should be displayed in this tabpanel." + ); + is( + tabpanel.querySelector(".empty-notice").textContent, + L10N.getStr("paramsNoPayloadText"), + "The empty notice should be correct." + ); +} diff --git a/devtools/client/netmonitor/test/browser_net_content-type.js b/devtools/client/netmonitor/test/browser_net_content-type.js new file mode 100644 index 0000000000..df0bec0c23 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_content-type.js @@ -0,0 +1,358 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if different response content types are handled correctly. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + CONTENT_TYPE_WITHOUT_CACHE_URL, + { requestCount: 1 } + ); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { L10N } = windowRequire("devtools/client/netmonitor/src/utils/l10n"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS); + + for (const requestItem of document.querySelectorAll(".request-list-item")) { + const requestsListStatus = requestItem.querySelector(".status-code"); + requestItem.scrollIntoView(); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + } + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CONTENT_TYPE_SJS + "?fmt=xml", + { + status: 200, + statusText: "OK", + type: "xml", + fullMimeType: "text/xml; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 42), + time: true, + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[1], + "GET", + CONTENT_TYPE_SJS + "?fmt=css", + { + status: 200, + statusText: "OK", + type: "css", + fullMimeType: "text/css; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34), + time: true, + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[2], + "GET", + CONTENT_TYPE_SJS + "?fmt=js", + { + status: 200, + statusText: "OK", + type: "js", + fullMimeType: "application/javascript; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 34), + time: true, + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[3], + "GET", + CONTENT_TYPE_SJS + "?fmt=json", + { + status: 200, + statusText: "OK", + type: "json", + fullMimeType: "application/json; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29), + time: true, + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[4], + "GET", + CONTENT_TYPE_SJS + "?fmt=bogus", + { + status: 404, + statusText: "Not Found", + type: "html", + fullMimeType: "text/html; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 24), + time: true, + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[5], + "GET", + TEST_IMAGE, + { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "png", + fullMimeType: "image/png", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 580), + time: true, + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[6], + "GET", + CONTENT_TYPE_SJS + "?fmt=gzip", + { + status: 200, + statusText: "OK", + type: "plain", + fullMimeType: "text/plain", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 324), + size: L10N.getFormatStrWithNumbers("networkMenu.size.kB", 10.99), + time: true, + } + ); + + await selectIndexAndWaitForSourceEditor(monitor, 0); + await testResponseTab("xml"); + + await selectIndexAndWaitForSourceEditor(monitor, 1); + await testResponseTab("css"); + + await selectIndexAndWaitForSourceEditor(monitor, 2); + await testResponseTab("js"); + + await selectIndexAndWaitForJSONView(3); + await testResponseTab("json"); + + await selectIndexAndWaitForHtmlView(4); + await testResponseTab("html"); + + await selectIndexAndWaitForImageView(5); + await testResponseTab("png"); + + await selectIndexAndWaitForSourceEditor(monitor, 6); + await testResponseTab("gzip"); + + await teardown(monitor); + + function testResponseTab(type) { + const tabpanel = document.querySelector("#response-panel"); + + function checkVisibility(box) { + is( + tabpanel.querySelector(".response-error-header") === null, + true, + "The response error header doesn't display" + ); + const jsonView = tabpanel.querySelector(".data-label") || {}; + is( + jsonView.textContent !== L10N.getStr("jsonScopeName"), + box != "json", + "The response json view doesn't display" + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + box !== "textarea", + "The response editor doesn't display" + ); + is( + tabpanel.querySelector(".response-image-box") === null, + box != "image", + "The response image view doesn't display" + ); + } + + switch (type) { + case "xml": { + checkVisibility("textarea"); + + const text = getCodeMirrorValue(monitor); + + is( + text, + "<label value='greeting'>Hello XML!</label>", + "The text shown in the source editor is incorrect for the xml request." + ); + break; + } + case "css": { + checkVisibility("textarea"); + + const text = getCodeMirrorValue(monitor); + + is( + text, + "body:pre { content: 'Hello CSS!' }", + "The text shown in the source editor is incorrect for the css request." + ); + break; + } + case "js": { + checkVisibility("textarea"); + + const text = getCodeMirrorValue(monitor); + + is( + text, + "function() { return 'Hello JS!'; }", + "The text shown in the source editor is incorrect for the js request." + ); + break; + } + case "json": { + checkVisibility("json"); + + is( + tabpanel.querySelectorAll(".raw-data-toggle").length, + 1, + "The response payload toggle should be displayed in this tabpanel." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + + is( + tabpanel.querySelector(".data-label").textContent, + L10N.getStr("jsonScopeName"), + "The json view section doesn't have the correct title." + ); + + const labels = tabpanel.querySelectorAll( + "tr .treeLabelCell .treeLabel" + ); + const values = tabpanel.querySelectorAll( + "tr .treeValueCell .objectBox" + ); + + is( + labels[0].textContent, + "greeting", + "The first json property name was incorrect." + ); + is( + values[0].textContent, + `"Hello JSON!"`, + "The first json property value was incorrect." + ); + break; + } + case "html": { + checkVisibility("html"); + + const text = document.querySelector(".html-preview iframe").src; + const expectedText = + "data:text/html;charset=UTF-8," + + encodeURIComponent("<blink>Not Found</blink>"); + + is( + text, + expectedText, + "The text shown in the iframe is incorrect for the html request." + ); + break; + } + case "png": { + checkVisibility("image"); + + const [name, dimensions, mime] = tabpanel.querySelectorAll( + ".response-image-box .tabpanel-summary-value" + ); + + is( + name.textContent, + "test-image.png", + "The image name info isn't correct." + ); + is(mime.textContent, "image/png", "The image mime info isn't correct."); + is( + dimensions.textContent, + "16" + " \u00D7 " + "16", + "The image dimensions info isn't correct." + ); + break; + } + case "gzip": { + checkVisibility("textarea"); + + const text = getCodeMirrorValue(monitor); + + is( + text, + new Array(1000).join("Hello gzip!"), + "The text shown in the source editor is incorrect for the gzip request." + ); + break; + } + } + } + + async function selectIndexAndWaitForHtmlView(index) { + const onResponseContent = monitor.panelWin.api.once( + TEST_EVENTS.RECEIVED_RESPONSE_CONTENT + ); + const tabpanel = document.querySelector("#response-panel"); + const waitDOM = waitForDOM(tabpanel, ".html-preview"); + store.dispatch(Actions.selectRequestByIndex(index)); + await waitDOM; + await onResponseContent; + } + + async function selectIndexAndWaitForJSONView(index) { + const onResponseContent = monitor.panelWin.api.once( + TEST_EVENTS.RECEIVED_RESPONSE_CONTENT + ); + const tabpanel = document.querySelector("#response-panel"); + const waitDOM = waitForDOM(tabpanel, ".treeTable"); + store.dispatch(Actions.selectRequestByIndex(index)); + await waitDOM; + await onResponseContent; + + // Waiting for RECEIVED_RESPONSE_CONTENT isn't enough. + // DOM may not be fully updated yet and checkVisibility(json) may still fail. + await waitForTick(); + } + + async function selectIndexAndWaitForImageView(index) { + const onResponseContent = monitor.panelWin.api.once( + TEST_EVENTS.RECEIVED_RESPONSE_CONTENT + ); + const tabpanel = document.querySelector("#response-panel"); + const waitDOM = waitForDOM(tabpanel, ".response-image"); + store.dispatch(Actions.selectRequestByIndex(index)); + const [imageNode] = await waitDOM; + await once(imageNode, "load"); + await onResponseContent; + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_cookies_sorted.js b/devtools/client/netmonitor/test/browser_net_cookies_sorted.js new file mode 100644 index 0000000000..f25220f650 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_cookies_sorted.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if Request-Cookies and Response-Cookies are sorted in Cookies tab. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_UNSORTED_COOKIES_SJS, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + wait = waitForDOM(document, ".headers-overview"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + clickOnSidebarTab(document, "cookies"); + + info("Check if Request-Cookies and Response-Cookies are sorted"); + const expectedLabelValues = [ + "__proto__", + "httpOnly", + "value", + "bob", + "httpOnly", + "value", + "foo", + "httpOnly", + "value", + "tom", + "httpOnly", + "value", + "__proto__", + "bob", + "foo", + "tom", + ]; + + const labelCells = document.querySelectorAll(".treeLabelCell"); + labelCells.forEach(function (val, index) { + is( + val.innerText, + expectedLabelValues[index], + "Actual label value " + + val.innerText + + " not equal to expected label value " + + expectedLabelValues[index] + ); + }); + + const lastItem = document.querySelector( + "#cookies-panel .properties-view tr.treeRow:last-child" + ); + lastItem.scrollIntoView(); + + info("Checking for unwanted scrollbars appearing in the tree view"); + const view = document.querySelector( + "#cookies-panel .properties-view .treeTable" + ); + is(scrolledToBottom(view), true, "The view is not scrollable"); + + await teardown(monitor); + + function scrolledToBottom(element) { + return element.scrollTop + element.clientHeight >= element.scrollHeight; + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_copy_as_curl.js b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js new file mode 100644 index 0000000000..4cce679fe9 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if Copy as cURL works. + */ + +const POST_PAYLOAD = "Plaintext value as a payload"; + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CURL_URL, { + requestCount: 1, + }); + info("Starting test... "); + + // Different quote chars are used for Windows and POSIX + const QUOTE_WIN = '"'; + const QUOTE_POSIX = "'"; + + const isWin = Services.appinfo.OS === "WINNT"; + const testData = isWin + ? [ + { + menuItemId: "request-list-context-copy-as-curl-win", + data: buildTestData(QUOTE_WIN), + }, + { + menuItemId: "request-list-context-copy-as-curl-posix", + data: buildTestData(QUOTE_POSIX), + }, + ] + : [ + { + menuItemId: "request-list-context-copy-as-curl", + data: buildTestData(QUOTE_POSIX), + }, + ]; + + await testForPlatform(tab, monitor, testData); + + await teardown(monitor); +}); + +function buildTestData(QUOTE) { + // Quote a string, escape the quotes inside the string + function quote(str) { + return QUOTE + str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`) + QUOTE; + } + + // Header param is formatted as -H "Header: value" or -H 'Header: value' + function header(h) { + return "-H " + quote(h); + } + + // Construct the expected command + const SIMPLE_BASE = ["curl " + quote(HTTPS_SIMPLE_SJS)]; + const SLOW_BASE = ["curl " + quote(HTTPS_SLOW_SJS)]; + const BASE_RESULT = [ + "--compressed", + header("User-Agent: " + navigator.userAgent), + header("Accept: */*"), + header("Accept-Language: " + navigator.language), + header("X-Custom-Header-1: Custom value"), + header("X-Custom-Header-2: 8.8.8.8"), + header("X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"), + header("Referer: " + HTTPS_CURL_URL), + header("Connection: keep-alive"), + header("Pragma: no-cache"), + header("Cache-Control: no-cache"), + header("Sec-Fetch-Dest: empty"), + header("Sec-Fetch-Mode: cors"), + header("Sec-Fetch-Site: same-origin"), + ]; + + const COOKIE_PARTIAL_RESULT = [header("Cookie: bob=true; tom=cool")]; + + const POST_PARTIAL_RESULT = [ + "-X", + "POST", + "--data-raw " + quote(POST_PAYLOAD), + header("Content-Type: text/plain;charset=UTF-8"), + ]; + const ORIGIN_RESULT = [header("Origin: https://example.com")]; + + const HEAD_PARTIAL_RESULT = ["-I"]; + + return { + SIMPLE_BASE, + SLOW_BASE, + BASE_RESULT, + COOKIE_PARTIAL_RESULT, + POST_PAYLOAD, + POST_PARTIAL_RESULT, + ORIGIN_RESULT, + HEAD_PARTIAL_RESULT, + }; +} + +async function testForPlatform(tab, monitor, testData) { + // GET request, no cookies (first request) + await performRequest("GET"); + for (const test of testData) { + await testClipboardContent(test.menuItemId, [ + ...test.data.SIMPLE_BASE, + ...test.data.BASE_RESULT, + ]); + } + // Check to make sure it is still OK after we view the response (bug#1452442) + await selectIndexAndWaitForSourceEditor(monitor, 0); + for (const test of testData) { + await testClipboardContent(test.menuItemId, [ + ...test.data.SIMPLE_BASE, + ...test.data.BASE_RESULT, + ]); + } + + // GET request, cookies set by previous response + await performRequest("GET"); + for (const test of testData) { + await testClipboardContent(test.menuItemId, [ + ...test.data.SIMPLE_BASE, + ...test.data.BASE_RESULT, + ...test.data.COOKIE_PARTIAL_RESULT, + ]); + } + + // Unfinished request (bug#1378464, bug#1420513) + const waitSlow = waitForNetworkEvents(monitor, 0); + await SpecialPowers.spawn( + tab.linkedBrowser, + [HTTPS_SLOW_SJS], + async function (url) { + content.wrappedJSObject.performRequest(url, "GET", null); + } + ); + await waitSlow; + for (const test of testData) { + await testClipboardContent(test.menuItemId, [ + ...test.data.SLOW_BASE, + ...test.data.BASE_RESULT, + ...test.data.COOKIE_PARTIAL_RESULT, + ]); + } + + // POST request + await performRequest("POST", POST_PAYLOAD); + for (const test of testData) { + await testClipboardContent(test.menuItemId, [ + ...test.data.SIMPLE_BASE, + ...test.data.BASE_RESULT, + ...test.data.COOKIE_PARTIAL_RESULT, + ...test.data.POST_PARTIAL_RESULT, + ...test.data.ORIGIN_RESULT, + ]); + } + + // HEAD request + await performRequest("HEAD"); + for (const test of testData) { + await testClipboardContent(test.menuItemId, [ + ...test.data.SIMPLE_BASE, + ...test.data.BASE_RESULT, + ...test.data.COOKIE_PARTIAL_RESULT, + ...test.data.HEAD_PARTIAL_RESULT, + ]); + } + + async function performRequest(method, payload) { + const waitRequest = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + url: HTTPS_SIMPLE_SJS, + method_: method, + payload_: payload, + }, + ], + async function ({ url, method_, payload_ }) { + content.wrappedJSObject.performRequest(url, method_, payload_); + } + ); + await waitRequest; + } + + async function testClipboardContent(menuItemId, expectedResult) { + const { document } = monitor.panelWin; + + const items = document.querySelectorAll(".request-list-item"); + const itemIndex = items.length - 1; + EventUtils.sendMouseEvent({ type: "mousedown" }, items[itemIndex]); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[0] + ); + + /* Ensure that the copy as cURL option is always visible */ + is( + !!getContextMenuItem(monitor, menuItemId), + true, + `The "Copy as cURL" context menu item "${menuItemId}" should not be hidden.` + ); + + await waitForClipboardPromise( + async function setup() { + await selectContextMenuItem(monitor, menuItemId); + }, + function validate(result) { + if (typeof result !== "string") { + return false; + } + + // Different setups may produce the same command, but with the + // parameters in a different order in the commandline (which is fine). + // Here we confirm that the commands are the same even in that case. + + // This monster regexp parses the command line into an array of arguments, + // recognizing quoted args with matching quotes and escaped quotes inside: + // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ] + const matchRe = /[-A-Za-z1-9]+(?: ([\"'])(?:\\\1|.)*?\1)?/g; + + const actual = result.match(matchRe); + // Must begin with the same "curl 'URL'" segment + if (!actual || expectedResult[0] != actual[0]) { + return false; + } + + // Must match each of the params in the middle (headers) + return ( + expectedResult.length === actual.length && + expectedResult.some(param => actual.includes(param)) + ); + } + ); + + info( + `Clipboard contains a cURL command for item ${itemIndex} by "${menuItemId}"` + ); + } +} diff --git a/devtools/client/netmonitor/test/browser_net_copy_as_fetch.js b/devtools/client/netmonitor/test/browser_net_copy_as_fetch.js new file mode 100644 index 0000000000..517e9e54f8 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_as_fetch.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if Copy as Fetch works. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CURL_URL, { + requestCount: 1, + }); + info("Starting test... "); + + // GET request, no cookies (first request) + await performRequest("GET"); + await testClipboardContent(`await fetch("https://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs", { + "credentials": "omit", + "headers": { + "User-Agent": "${navigator.userAgent}", + "Accept": "*/*", + "Accept-Language": "en-US", + "X-Custom-Header-1": "Custom value", + "X-Custom-Header-2": "8.8.8.8", + "X-Custom-Header-3": "Mon, 3 Mar 2014 11:11:11 GMT", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + }, + "referrer": "https://example.com/browser/devtools/client/netmonitor/test/html_copy-as-curl.html", + "method": "GET", + "mode": "cors" +});`); + + await teardown(monitor); + + async function performRequest(method, payload) { + const waitRequest = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + url: HTTPS_SIMPLE_SJS, + method_: method, + payload_: payload, + }, + ], + async function ({ url, method_, payload_ }) { + content.wrappedJSObject.performRequest(url, method_, payload_); + } + ); + await waitRequest; + } + + async function testClipboardContent(expectedResult) { + const { document } = monitor.panelWin; + + const items = document.querySelectorAll(".request-list-item"); + EventUtils.sendMouseEvent({ type: "mousedown" }, items[items.length - 1]); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[0] + ); + + /* Ensure that the copy as fetch option is always visible */ + is( + !!getContextMenuItem(monitor, "request-list-context-copy-as-fetch"), + true, + 'The "Copy as Fetch" context menu item should not be hidden.' + ); + + await waitForClipboardPromise( + async function setup() { + await selectContextMenuItem( + monitor, + "request-list-context-copy-as-fetch" + ); + }, + function validate(result) { + if (typeof result !== "string") { + return false; + } + return expectedResult === result; + } + ); + + info("Clipboard contains a fetch command for item " + (items.length - 1)); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_copy_as_powershell.js b/devtools/client/netmonitor/test/browser_net_copy_as_powershell.js new file mode 100644 index 0000000000..5785b89929 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_as_powershell.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the Copy as PowerShell command + */ +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CURL_URL, { + requestCount: 1, + }); + + info("Starting test... "); + info("Test powershell command for GET request without any cookies"); + await performRequest("GET"); + await testClipboardContentForRecentRequest(`Invoke-WebRequest -UseBasicParsing -Uri "https://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs" \` +-UserAgent "${navigator.userAgent}" \` +-Headers @{ +"Accept" = "*/*" + "Accept-Language" = "en-US" + "Accept-Encoding" = "gzip, deflate, br" + "X-Custom-Header-1" = "Custom value" + "X-Custom-Header-2" = "8.8.8.8" + "X-Custom-Header-3" = "Mon, 3 Mar 2014 11:11:11 GMT" + "Referer" = "https://example.com/browser/devtools/client/netmonitor/test/html_copy-as-curl.html" + "Sec-Fetch-Dest" = "empty" + "Sec-Fetch-Mode" = "cors" + "Sec-Fetch-Site" = "same-origin" + "Pragma" = "no-cache" + "Cache-Control" = "no-cache" +}`); + + info("Test powershell command for GET request with cookies"); + await performRequest("GET"); + await testClipboardContentForRecentRequest(`$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession +$session.Cookies.Add((New-Object System.Net.Cookie("bob", "true", "/", "example.com"))) +$session.Cookies.Add((New-Object System.Net.Cookie("tom", "cool", "/", "example.com"))) +Invoke-WebRequest -UseBasicParsing -Uri "https://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs" \` +-WebSession $session \` +-UserAgent "${navigator.userAgent}" \` +-Headers @{ +"Accept" = "*/*" + "Accept-Language" = "en-US" + "Accept-Encoding" = "gzip, deflate, br" + "X-Custom-Header-1" = "Custom value" + "X-Custom-Header-2" = "8.8.8.8" + "X-Custom-Header-3" = "Mon, 3 Mar 2014 11:11:11 GMT" + "Referer" = "https://example.com/browser/devtools/client/netmonitor/test/html_copy-as-curl.html" + "Sec-Fetch-Dest" = "empty" + "Sec-Fetch-Mode" = "cors" + "Sec-Fetch-Site" = "same-origin" + "Pragma" = "no-cache" + "Cache-Control" = "no-cache" +}`); + + info("Test powershell command for POST request with post body"); + await performRequest("POST", "Plaintext value as a payload"); + await testClipboardContentForRecentRequest(`$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession +$session.Cookies.Add((New-Object System.Net.Cookie("bob", "true", "/", "example.com"))) +$session.Cookies.Add((New-Object System.Net.Cookie("tom", "cool", "/", "example.com"))) +Invoke-WebRequest -UseBasicParsing -Uri "https://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs" \` +-Method POST \` +-WebSession $session \` +-UserAgent "${navigator.userAgent}" \` +-Headers @{ +"Accept" = "*/*" + "Accept-Language" = "en-US" + "Accept-Encoding" = "gzip, deflate, br" + "X-Custom-Header-1" = "Custom value" + "X-Custom-Header-2" = "8.8.8.8" + "X-Custom-Header-3" = "Mon, 3 Mar 2014 11:11:11 GMT" + "Origin" = "https://example.com" + "Referer" = "https://example.com/browser/devtools/client/netmonitor/test/html_copy-as-curl.html" + "Sec-Fetch-Dest" = "empty" + "Sec-Fetch-Mode" = "cors" + "Sec-Fetch-Site" = "same-origin" + "Pragma" = "no-cache" + "Cache-Control" = "no-cache" +} \` +-ContentType "text/plain;charset=UTF-8" \` +-Body "Plaintext value as a payload"`); + + info( + "Test powershell command for POST request with post body which contains ASCII non printing characters" + ); + await performRequest("POST", `TAB character included in payload \t`); + await testClipboardContentForRecentRequest(`$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession +$session.Cookies.Add((New-Object System.Net.Cookie("bob", "true", "/", "example.com"))) +$session.Cookies.Add((New-Object System.Net.Cookie("tom", "cool", "/", "example.com"))) +Invoke-WebRequest -UseBasicParsing -Uri "https://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs" \` +-Method POST \` +-WebSession $session \` +-UserAgent "${navigator.userAgent}" \` +-Headers @{ +"Accept" = "*/*" + "Accept-Language" = "en-US" + "Accept-Encoding" = "gzip, deflate, br" + "X-Custom-Header-1" = "Custom value" + "X-Custom-Header-2" = "8.8.8.8" + "X-Custom-Header-3" = "Mon, 3 Mar 2014 11:11:11 GMT" + "Origin" = "https://example.com" + "Referer" = "https://example.com/browser/devtools/client/netmonitor/test/html_copy-as-curl.html" + "Sec-Fetch-Dest" = "empty" + "Sec-Fetch-Mode" = "cors" + "Sec-Fetch-Site" = "same-origin" + "Pragma" = "no-cache" + "Cache-Control" = "no-cache" +} \` +-ContentType "text/plain;charset=UTF-8" \` +-Body ([System.Text.Encoding]::UTF8.GetBytes("TAB character included in payload $([char]9)"))`); + + async function performRequest(method, payload) { + const waitRequest = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + url: HTTPS_SIMPLE_SJS, + method_: method, + payload_: payload, + }, + ], + async function ({ url, method_, payload_ }) { + content.wrappedJSObject.performRequest(url, method_, payload_); + } + ); + await waitRequest; + } + + async function testClipboardContentForRecentRequest(expectedClipboardText) { + const { document } = monitor.panelWin; + + const items = document.querySelectorAll(".request-list-item"); + EventUtils.sendMouseEvent({ type: "mousedown" }, items[items.length - 1]); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[0] + ); + + /* Ensure that the copy as fetch option is always visible */ + is( + !!getContextMenuItem(monitor, "request-list-context-copy-as-powershell"), + true, + 'The "Copy as PowerShell" context menu item should not be hidden on windows' + ); + + await waitForClipboardPromise( + async function setup() { + await selectContextMenuItem( + monitor, + "request-list-context-copy-as-powershell" + ); + }, + function validate(result) { + if (typeof result !== "string") { + return false; + } + return expectedClipboardText == result; + } + ); + + info( + "Clipboard contains a powershell command for item " + (items.length - 1) + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_copy_headers.js b/devtools/client/netmonitor/test/browser_net_copy_headers.js new file mode 100644 index 0000000000..cd6499d93b --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_headers.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if copying a request's request/response headers works. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { getSortedRequests, getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + + const requestItem = getSortedRequests(store.getState())[0]; + const { method, httpVersion, status, statusText } = requestItem; + + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[0] + ); + + const selectedRequest = getSelectedRequest(store.getState()); + is(selectedRequest, requestItem, "Proper request is selected"); + + const EXPECTED_REQUEST_HEADERS = [ + `${method} ${SIMPLE_URL.split("example.com")[1]} ${httpVersion}`, + "Host: example.com", + "User-Agent: " + navigator.userAgent + "", + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language: " + navigator.languages.join(",") + ";q=0.5", + "Accept-Encoding: gzip, deflate", + "Connection: keep-alive", + "Upgrade-Insecure-Requests: 1", + "Pragma: no-cache", + "Cache-Control: no-cache", + ].join("\n"); + + await waitForClipboardPromise( + async function setup() { + await selectContextMenuItem( + monitor, + "request-list-context-copy-request-headers" + ); + }, + function validate(result) { + // Sometimes, a "Cookie" header is left over from other tests. Remove it: + result = String(result).replace(/Cookie: [^\n]+\n/, ""); + return result === EXPECTED_REQUEST_HEADERS; + } + ); + info("Clipboard contains the currently selected item's request headers."); + + const EXPECTED_RESPONSE_HEADERS = [ + `${httpVersion} ${status} ${statusText}`, + "last-modified: Sun, 3 May 2015 11:11:11 GMT", + "content-type: text/html", + "content-length: 465", + "connection: close", + "server: httpd.js", + "date: Sun, 3 May 2015 11:11:11 GMT", + ].join("\n"); + + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[0] + ); + + await waitForClipboardPromise( + async function setup() { + await selectContextMenuItem( + monitor, + "response-list-context-copy-response-headers" + ); + }, + function validate(result) { + // Fake the "Last-Modified" and "Date" headers because they will vary: + result = String(result) + .replace( + /last-modified: [^\n]+ GMT/, + "last-modified: Sun, 3 May 2015 11:11:11 GMT" + ) + .replace(/date: [^\n]+ GMT/, "date: Sun, 3 May 2015 11:11:11 GMT"); + return result === EXPECTED_RESPONSE_HEADERS; + } + ); + info("Clipboard contains the currently selected item's response headers."); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js b/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js new file mode 100644 index 0000000000..88c080d707 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_image_as_data_uri.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if copying an image as data uri works. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + CONTENT_TYPE_WITHOUT_CACHE_URL, + { requestCount: 1 } + ); + info("Starting test... "); + + const { document } = monitor.panelWin; + + // Execute requests. + await performRequests(monitor, tab, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[5] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[5] + ); + + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "request-list-context-copy-image-as-data-uri" + ); + }, TEST_IMAGE_DATA_URI); + + ok(true, "Clipboard contains the currently selected image as data uri."); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_copy_params.js b/devtools/client/netmonitor/test/browser_net_copy_params.js new file mode 100644 index 0000000000..b1f4006821 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_params.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether copying a request item's parameters works. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(PARAMS_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 7); + + await testCopyUrlParamsHidden(0, false); + await testCopyUrlParams(0, "a"); + await testCopyPostDataHidden(0, false); + await testCopyPostData(0, '{ "foo": "bar" }'); + + await testCopyUrlParamsHidden(1, false); + await testCopyUrlParams(1, "a=b"); + await testCopyPostDataHidden(1, false); + await testCopyPostData(1, '{ "foo": "bar" }'); + + await testCopyUrlParamsHidden(2, false); + await testCopyUrlParams(2, "a=b"); + await testCopyPostDataHidden(2, false); + await testCopyPostData(2, "foo=bar=123=xyz"); + + await testCopyUrlParamsHidden(3, false); + await testCopyUrlParams(3, "a"); + await testCopyPostDataHidden(3, false); + await testCopyPostData(3, '{ "foo": "bar" }'); + + await testCopyUrlParamsHidden(4, false); + await testCopyUrlParams(4, "a=b"); + await testCopyPostDataHidden(4, false); + await testCopyPostData(4, '{ "foo": "bar" }'); + + await testCopyUrlParamsHidden(5, false); + await testCopyUrlParams(5, "a=b"); + await testCopyPostDataHidden(5, false); + await testCopyPostData(5, "?foo=bar"); + testCopyRequestDataLabel(5, "POST"); + + await testCopyUrlParamsHidden(6, true); + await testCopyPostDataHidden(6, true); + + await testCopyPostDataHidden(7, false); + testCopyRequestDataLabel(7, "PATCH"); + + await testCopyPostDataHidden(8, false); + testCopyRequestDataLabel(8, "PUT"); + + return teardown(monitor); + + async function testCopyUrlParamsHidden(index, hidden) { + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[index] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[index] + ); + + is( + !!getContextMenuItem(monitor, "request-list-context-copy-url-params"), + !hidden, + 'The "Copy URL Parameters" context menu item should' + + (hidden ? " " : " not ") + + "be hidden." + ); + } + + async function testCopyUrlParams(index, queryString) { + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[index] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[index] + ); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "request-list-context-copy-url-params" + ); + }, queryString); + ok(true, "The url query string copied from the selected item is correct."); + } + + async function testCopyPostDataHidden(index, hidden) { + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[index] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[index] + ); + is( + !!getContextMenuItem(monitor, "request-list-context-copy-post-data"), + !hidden, + 'The "Copy POST Data" context menu item should' + + (hidden ? " " : " not ") + + "be hidden." + ); + } + + function testCopyRequestDataLabel(index, method) { + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[index] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[index] + ); + const copyPostDataNode = getContextMenuItem( + monitor, + "request-list-context-copy-post-data" + ); + is( + copyPostDataNode.attributes.label.value, + "Copy " + method + " Data", + 'The "Copy Data" context menu item should have label - Copy ' + + method + + " Data" + ); + } + + async function testCopyPostData(index, postData) { + // Wait for formDataSections and requestPostData state are ready in redux store + // since copyPostData API needs to read these state. + await waitUntil(() => { + const { requests } = store.getState().requests; + const { formDataSections, requestPostData } = requests[index]; + return formDataSections && requestPostData; + }); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[index] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[index] + ); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "request-list-context-copy-post-data" + ); + }, postData); + ok(true, "The post data string copied from the selected item is correct."); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_copy_response.js b/devtools/client/netmonitor/test/browser_net_copy_response.js new file mode 100644 index 0000000000..28a09001fc --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_response.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if copying a request's response works. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + CONTENT_TYPE_WITHOUT_CACHE_URL, + { requestCount: 1 } + ); + info("Starting test... "); + + const EXPECTED_RESULT = '{ "greeting": "Hello JSON!" }'; + + const { document } = monitor.panelWin; + + // Execute requests. + await performRequests(monitor, tab, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[3] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[3] + ); + + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem(monitor, "request-list-context-copy-response"); + }, EXPECTED_RESULT); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js b/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js new file mode 100644 index 0000000000..e01842c3db --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_svg_image_as_data_uri.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if copying an image as data uri works. + */ + +const SVG_URL = HTTPS_EXAMPLE_URL + "dropmarker.svg"; + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CURL_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document } = monitor.panelWin; + + const wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [SVG_URL], async function (url) { + content.wrappedJSObject.performRequest(url); + }); + await wait; + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[0] + ); + + await waitForClipboardPromise( + async function setup() { + await selectContextMenuItem( + monitor, + "request-list-context-copy-image-as-data-uri" + ); + }, + function check(text) { + return text.startsWith("data:") && !/undefined/.test(text); + } + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_copy_url.js b/devtools/client/netmonitor/test/browser_net_copy_url.js new file mode 100644 index 0000000000..41e73877ae --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_copy_url.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if copying a request's url works. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + // Execute requests. + await performRequests(monitor, tab, 1); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + + const requestItem = getSortedRequests(store.getState())[0]; + + info("Simulating CmdOrCtrl+C on a first element of the request table"); + await waitForClipboardPromise( + () => synthesizeKeyShortcut("CmdOrCtrl+C"), + requestItem.url + ); + + emptyClipboard(); + + info("Simulating context click on a first element of the request table"); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[0] + ); + + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem(monitor, "request-list-context-copy-url"); + }, requestItem.url); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_cors_requests.js b/devtools/client/netmonitor/test/browser_net_cors_requests.js new file mode 100644 index 0000000000..40668dca9a --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_cors_requests.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that CORS preflight requests are displayed by network monitor + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CORS_URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 2); + + info("Performing a CORS request"); + const requestUrl = "https://test1.example.com" + CORS_SJS_PATH; + await SpecialPowers.spawn( + tab.linkedBrowser, + [requestUrl], + async function (url) { + content.wrappedJSObject.performRequests( + url, + "triggering/preflight", + "post-data" + ); + } + ); + + info("Waiting until the requests appear in netmonitor"); + await wait; + + info("Checking the preflight and flight methods"); + ["OPTIONS", "POST"].forEach((method, index) => { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[index], + method, + requestUrl + ); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_curl-utils.js b/devtools/client/netmonitor/test/browser_net_curl-utils.js new file mode 100644 index 0000000000..32b7aca316 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js @@ -0,0 +1,385 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests Curl Utils functionality. + */ + +const { + Curl, + CurlUtils, +} = require("resource://devtools/client/shared/curl.js"); + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CURL_UTILS_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const { getLongString, requestData } = connector; + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 6); + await SpecialPowers.spawn( + tab.linkedBrowser, + [HTTPS_SIMPLE_SJS], + async function (url) { + content.wrappedJSObject.performRequests(url); + } + ); + await wait; + + const requests = { + get: getSortedRequests(store.getState())[0], + post: getSortedRequests(store.getState())[1], + postJson: getSortedRequests(store.getState())[2], + patch: getSortedRequests(store.getState())[3], + multipart: getSortedRequests(store.getState())[4], + multipartForm: getSortedRequests(store.getState())[5], + }; + + let data = await createCurlData(requests.get, getLongString, requestData); + testFindHeader(data); + + data = await createCurlData(requests.post, getLongString, requestData); + testIsUrlEncodedRequest(data); + testWritePostDataTextParams(data); + testWriteEmptyPostDataTextParams(data); + testDataArgumentOnGeneratedCommand(data); + + data = await createCurlData(requests.patch, getLongString, requestData); + testWritePostDataTextParams(data); + testDataArgumentOnGeneratedCommand(data); + + data = await createCurlData(requests.postJson, getLongString, requestData); + testDataEscapeOnGeneratedCommand(data); + + data = await createCurlData(requests.multipart, getLongString, requestData); + testIsMultipartRequest(data); + testGetMultipartBoundary(data); + testMultiPartHeaders(data); + testRemoveBinaryDataFromMultipartText(data); + + data = await createCurlData( + requests.multipartForm, + getLongString, + requestData + ); + testMultiPartHeaders(data); + + testGetHeadersFromMultipartText({ + postDataText: "Content-Type: text/plain\r\n\r\n", + }); + + if (Services.appinfo.OS != "WINNT") { + testEscapeStringPosix(); + } else { + testEscapeStringWin(); + } + + await teardown(monitor); +}); + +function testIsUrlEncodedRequest(data) { + const isUrlEncoded = CurlUtils.isUrlEncodedRequest(data); + ok(isUrlEncoded, "Should return true for url encoded requests."); +} + +function testIsMultipartRequest(data) { + const isMultipart = CurlUtils.isMultipartRequest(data); + ok(isMultipart, "Should return true for multipart/form-data requests."); +} + +function testFindHeader(data) { + const { headers } = data; + const hostName = CurlUtils.findHeader(headers, "Host"); + const requestedWithLowerCased = CurlUtils.findHeader( + headers, + "x-requested-with" + ); + const doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist"); + + is( + hostName, + "example.com", + "Header with name 'Host' should be found in the request array." + ); + is( + requestedWithLowerCased, + "XMLHttpRequest", + "The search should be case insensitive." + ); + is(doesNotExist, null, "Should return null when a header is not found."); +} + +function testMultiPartHeaders(data) { + const { headers } = data; + const contentType = CurlUtils.findHeader(headers, "Content-Type"); + + ok( + contentType.startsWith("multipart/form-data; boundary="), + "Multi-part content type header is present in headers array" + ); +} + +function testWritePostDataTextParams(data) { + const params = CurlUtils.writePostDataTextParams(data.postDataText); + is( + params, + "param1=value1¶m2=value2¶m3=value3", + "Should return a serialized representation of the request parameters" + ); +} + +function testWriteEmptyPostDataTextParams(data) { + const params = CurlUtils.writePostDataTextParams(null); + is(params, "", "Should return a empty string when no parameters provided"); +} + +function testDataArgumentOnGeneratedCommand(data) { + const curlCommand = Curl.generateCommand(data); + ok( + curlCommand.includes("--data-raw"), + "Should return a curl command with --data-raw" + ); +} + +function testDataEscapeOnGeneratedCommand(data) { + const paramsWin = `--data-raw "{""param1"":""value1"",""param2"":""value2""}"`; + const paramsPosix = `--data-raw '{"param1":"value1","param2":"value2"}'`; + + let curlCommand = Curl.generateCommand(data, "WINNT"); + ok( + curlCommand.includes(paramsWin), + "Should return a curl command with --data-raw escaped for Windows systems" + ); + + curlCommand = Curl.generateCommand(data, "Linux"); + ok( + curlCommand.includes(paramsPosix), + "Should return a curl command with --data-raw escaped for Posix systems" + ); +} + +function testGetMultipartBoundary(data) { + const boundary = CurlUtils.getMultipartBoundary(data); + ok( + /-{3,}\w+/.test(boundary), + "A boundary string should be found in a multipart request." + ); +} + +function testRemoveBinaryDataFromMultipartText(data) { + const generatedBoundary = CurlUtils.getMultipartBoundary(data); + const text = data.postDataText; + const binaryRemoved = CurlUtils.removeBinaryDataFromMultipartText( + text, + generatedBoundary + ); + const boundary = "--" + generatedBoundary; + + const EXPECTED_POSIX_RESULT = [ + "$'", + boundary, + "\\r\\n", + 'Content-Disposition: form-data; name="param1"', + "\\r\\n\\r\\n", + "value1", + "\\r\\n", + boundary, + "\\r\\n", + 'Content-Disposition: form-data; name="file"; filename="filename.png"', + "\\r\\n", + "Content-Type: image/png", + "\\r\\n\\r\\n", + boundary + "--", + "\\r\\n", + "'", + ].join(""); + + const EXPECTED_WIN_RESULT = [ + '"', + boundary, + '"^\u000d\u000A\u000d\u000A"', + 'Content-Disposition: form-data; name=""param1""', + '"^\u000d\u000A\u000d\u000A""^\u000d\u000A\u000d\u000A"', + "value1", + '"^\u000d\u000A\u000d\u000A"', + boundary, + '"^\u000d\u000A\u000d\u000A"', + 'Content-Disposition: form-data; name=""file""; filename=""filename.png""', + '"^\u000d\u000A\u000d\u000A"', + "Content-Type: image/png", + '"^\u000d\u000A\u000d\u000A""^\u000d\u000A\u000d\u000A"', + boundary + "--", + '"^\u000d\u000A\u000d\u000A"', + '"', + ].join(""); + + if (Services.appinfo.OS != "WINNT") { + is( + CurlUtils.escapeStringPosix(binaryRemoved), + EXPECTED_POSIX_RESULT, + "The mulitpart request payload should not contain binary data." + ); + } else { + is( + CurlUtils.escapeStringWin(binaryRemoved), + EXPECTED_WIN_RESULT, + "WinNT: The mulitpart request payload should not contain binary data." + ); + } +} + +function testGetHeadersFromMultipartText(data) { + const headers = CurlUtils.getHeadersFromMultipartText(data.postDataText); + + ok(Array.isArray(headers), "Should return an array."); + ok(!!headers.length, "There should exist at least one request header."); + is( + headers[0].name, + "Content-Type", + "The first header name should be 'Content-Type'." + ); +} + +function testEscapeStringPosix() { + const surroundedWithQuotes = "A simple string"; + is( + CurlUtils.escapeStringPosix(surroundedWithQuotes), + "'A simple string'", + "The string should be surrounded with single quotes." + ); + + const singleQuotes = "It's unusual to put crickets in your coffee."; + is( + CurlUtils.escapeStringPosix(singleQuotes), + "$'It\\'s unusual to put crickets in your coffee.'", + "Single quotes should be escaped." + ); + + const escapeChar = "'!ls:q:gs|ls|;ping 8.8.8.8;|"; + is( + CurlUtils.escapeStringPosix(escapeChar), + "$'\\'\\041ls:q:gs|ls|;ping 8.8.8.8;|'", + "'!' should be escaped." + ); + + const newLines = "Line 1\r\nLine 2\u000d\u000ALine3"; + is( + CurlUtils.escapeStringPosix(newLines), + "$'Line 1\\r\\nLine 2\\r\\nLine3'", + "Newlines should be escaped." + ); + + const controlChars = "\u0007 \u0009 \u000C \u001B"; + is( + CurlUtils.escapeStringPosix(controlChars), + "$'\\x07 \\x09 \\x0c \\x1b'", + "Control characters should be escaped." + ); + + // æ ø ü ß ö é + const extendedAsciiChars = + "\xc3\xa6 \xc3\xb8 \xc3\xbc \xc3\x9f \xc3\xb6 \xc3\xa9"; + is( + CurlUtils.escapeStringPosix(extendedAsciiChars), + "$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'", + "Character codes outside of the decimal range 32 - 126 should be escaped." + ); +} + +function testEscapeStringWin() { + const surroundedWithDoubleQuotes = "A simple string"; + is( + CurlUtils.escapeStringWin(surroundedWithDoubleQuotes), + '"A simple string"', + "The string should be surrounded with double quotes." + ); + + const doubleQuotes = 'Quote: "Time is an illusion. Lunchtime doubly so."'; + is( + CurlUtils.escapeStringWin(doubleQuotes), + '"Quote: ""Time is an illusion. Lunchtime doubly so."""', + "Double quotes should be escaped." + ); + + const percentSigns = "%TEMP% %@foo% %2XX% %_XX% %?XX%"; + is( + CurlUtils.escapeStringWin(percentSigns), + '"^%^TEMP^% ^%^@foo^% ^%^2XX^% ^%^_XX^% ^%?XX^%"', + "Percent signs should be escaped." + ); + + const backslashes = "\\A simple string\\"; + is( + CurlUtils.escapeStringWin(backslashes), + '"\\\\A simple string\\\\"', + "Backslashes should be escaped." + ); + + const newLines = "line1\r\nline2\r\rline3\n\nline4"; + is( + CurlUtils.escapeStringWin(newLines), + '"line1"^\r\n\r\n"line2"^\r\n\r\n""^\r\n\r\n"line3"^\r\n\r\n""^\r\n\r\n"line4"', + "Newlines should be escaped." + ); + + const dollarSignCommand = "$(calc.exe)"; + is( + CurlUtils.escapeStringWin(dollarSignCommand), + '"\\$(calc.exe)"', + "Dollar sign should be escaped." + ); + + const tickSignCommand = "`$(calc.exe)"; + is( + CurlUtils.escapeStringWin(tickSignCommand), + '"\\`\\$(calc.exe)"', + "Both the tick and dollar signs should be escaped." + ); + + const evilCommand = `query=evil\r\rcmd" /c timeout /t 3 & calc.exe\r\r`; + is( + CurlUtils.escapeStringWin(evilCommand), + '"query=evil"^\r\n\r\n""^\r\n\r\n"cmd"" /c timeout /t 3 & calc.exe"^\r\n\r\n""^\r\n\r\n""', + "The evil command is escaped properly" + ); +} + +async function createCurlData(selected, getLongString, requestData) { + const { id, url, method, httpVersion } = selected; + + // Create a sanitized object for the Curl command generator. + const data = { + url, + method, + headers: [], + httpVersion, + postDataText: null, + }; + + const requestHeaders = await requestData(id, "requestHeaders"); + // Fetch header values. + for (const { name, value } of requestHeaders.headers) { + const text = await getLongString(value); + data.headers.push({ name, value: text }); + } + + const requestPostData = await requestData(id, "requestPostData"); + // Fetch the request payload. + if (requestPostData) { + const postData = requestPostData.postData.text; + data.postDataText = await getLongString(postData); + } + + return data; +} diff --git a/devtools/client/netmonitor/test/browser_net_cyrillic-01.js b/devtools/client/netmonitor/test/browser_net_cyrillic-01.js new file mode 100644 index 0000000000..4b44dc34ac --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_cyrillic-01.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if cyrillic text is rendered correctly in the source editor. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CYRILLIC_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const requestItem = document.querySelectorAll(".request-list-item")[0]; + const requestsListStatus = requestItem.querySelector(".status-code"); + requestItem.scrollIntoView(); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CONTENT_TYPE_SJS + "?fmt=txt", + { + status: 200, + statusText: "DA DA DA", + } + ); + + let wait = waitForDOM(document, "#headers-panel"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + clickOnSidebarTab(document, "response"); + await wait; + + ok( + getCodeMirrorValue(monitor).includes( + "\u0411\u0440\u0430\u0442\u0430\u043d" + ), + "The text shown in the source editor is correct." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_cyrillic-02.js b/devtools/client/netmonitor/test/browser_net_cyrillic-02.js new file mode 100644 index 0000000000..ee8f8946c9 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_cyrillic-02.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if cyrillic text is rendered correctly in the source editor + * when loaded directly from an HTML page. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(CYRILLIC_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + const requestItem = document.querySelectorAll(".request-list-item")[0]; + const requestsListStatus = requestItem.querySelector(".status-code"); + requestItem.scrollIntoView(); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CYRILLIC_URL, + { + status: 200, + statusText: "OK", + } + ); + + wait = waitForDOM(document, "#headers-panel"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + wait = waitForDOM(document, "#response-panel .data-header"); + clickOnSidebarTab(document, "response"); + await wait; + + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + const header = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(header, monitor); + await wait; + + // CodeMirror will only load lines currently in view to the DOM. getValue() + // retrieves all lines pending render after a user begins scrolling. + const text = document.querySelector(".CodeMirror").CodeMirror.getValue(); + + ok( + text.includes("\u0411\u0440\u0430\u0442\u0430\u043d"), + "The text shown in the source editor is correct." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_decode-params.js b/devtools/client/netmonitor/test/browser_net_decode-params.js new file mode 100644 index 0000000000..45be876dec --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_decode-params.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if "+" is replaces with spaces in the headers panel. + */ +add_task(async function () { + const { tab, monitor } = await initNetMonitor(POST_RAW_URL_WITH_HASH, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Execute request. + await performRequests(monitor, tab, 1); + + // Wait until the tab panel summary is displayed + const wait = waitUntil( + () => document.querySelectorAll(".tabpanel-summary-label")[0] + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + clickOnSidebarTab(document, "request"); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_decode-url.js b/devtools/client/netmonitor/test/browser_net_decode-url.js new file mode 100644 index 0000000000..e3a900f313 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_decode-url.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if "Request URL" containing "#" in its query is correctly decoded. + */ +add_task(async function () { + const { tab, monitor } = await initNetMonitor(POST_RAW_URL_WITH_HASH, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Execute request. + await performRequests(monitor, tab, 1); + + // Wait until the url preview ihas loaded + const wait = waitUntil(() => + document.querySelector("#headers-panel .url-preview") + ); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + + await wait; + + const requestURL = document.querySelector("#headers-panel .url-preview .url"); + is( + requestURL.textContent.endsWith("foo # bar"), + true, + "\"Request URL\" containing '#' is correctly decoded." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_details_copy.js b/devtools/client/netmonitor/test/browser_net_details_copy.js new file mode 100644 index 0000000000..7f0c53a809 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_details_copy.js @@ -0,0 +1,296 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/** + * Test that the URL Preview can be copied + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + + info("Starting the url preview copy test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + store.dispatch(Actions.toggleNetworkDetails()); + + await waitForDOM(document, "#headers-panel .url-preview", 1); + + const urlPreview = document.querySelector("#headers-panel .url-preview"); + const urlRow = urlPreview.querySelector(".objectRow"); + + /* Test for copy value on the url */ + EventUtils.sendMouseEvent({ type: "contextmenu" }, urlRow); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "properties-view-context-menu-copyvalue" + ); + }, "http://example.com/browser/devtools/client/netmonitor/test/html_simple-test-page.html"); + + ok(true, "The copy value action put expected url string into clipboard"); + + /* Test for copy all */ + EventUtils.sendMouseEvent({ type: "contextmenu" }, urlRow); + const expected = JSON.stringify( + { + GET: { + scheme: "http", + host: "example.com", + filename: + "/browser/devtools/client/netmonitor/test/html_simple-test-page.html", + remote: { + Address: "127.0.0.1:8888", + }, + }, + }, + null, + "\t" + ); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "properties-view-context-menu-copyall" + ); + }, expected); + + ok(true, "The copy all action put expected json data into clipboard"); + + await teardown(monitor); +}); + +/** + * Test that the Headers summary can be copied + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + + info("Starting the headers summary copy test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + store.dispatch(Actions.toggleNetworkDetails()); + + await waitForDOM(document, "#headers-panel .summary", 1); + + const headersSummary = document.querySelector("#headers-panel .summary"); + const httpSummaryValue = headersSummary.querySelectorAll( + ".tabpanel-summary-value" + )[1]; + + /* Test for copy value */ + EventUtils.sendMouseEvent({ type: "contextmenu" }, httpSummaryValue); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "headers-panel-context-menu-copyvalue" + ); + }, "HTTP/1.1"); + + ok(true, "The copy value action put expected text into clipboard"); + + /* Test for copy all */ + EventUtils.sendMouseEvent({ type: "contextmenu" }, httpSummaryValue); + const expected = JSON.stringify( + { + Status: "200OK", + Version: "HTTP/1.1", + Transferred: "650 B (465 B size)", + "Request Priority": "Highest", + "DNS Resolution": "System", + }, + null, + "\t" + ); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem(monitor, "headers-panel-context-menu-copyall"); + }, expected); + + ok(true, "The copy all action put expected json into clipboard"); + + await teardown(monitor); +}); + +/** + * Test if response JSON in PropertiesView can be copied + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + JSON_BASIC_URL + "?name=nogrip", + { requestCount: 1 } + ); + info("Starting the json in properties view copy test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + await performRequests(monitor, tab, 1); + + const onResponsePanelReady = waitForDOM( + document, + "#response-panel .treeTable" + ); + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await onResponsePanelReady; + + const responsePanel = document.querySelector("#response-panel"); + + const objectRow = responsePanel.querySelectorAll(".objectRow")[0]; + + // Open the node to get the string + const waitOpenNode = waitForDOM(document, ".stringRow"); + const toggleButton = objectRow.querySelector("td span.treeIcon"); + toggleButton.click(); + await waitOpenNode; + const stringRow = responsePanel.querySelectorAll(".stringRow")[0]; + + /* Test for copy value on an object */ + EventUtils.sendMouseEvent({ type: "contextmenu" }, objectRow); + const expected = JSON.stringify({ obj: { type: "string" } }, null, "\t"); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "properties-view-context-menu-copyvalue" + ); + }, expected); + + ok(true, "The copy value action put expected json into clipboard"); + + /* Test for copy all */ + EventUtils.sendMouseEvent({ type: "contextmenu" }, objectRow); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "properties-view-context-menu-copyall" + ); + }, expected); + + ok(true, "The copy all action put expected json into clipboard"); + + /* Test for copy value of a single row */ + EventUtils.sendMouseEvent({ type: "contextmenu" }, stringRow); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "properties-view-context-menu-copyvalue" + ); + }, "string"); + + ok(true, "The copy value action put expected text into clipboard"); + + await teardown(monitor); +}); + +/** + * Test if response/request Cookies in PropertiesView can be copied + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_UNSORTED_COOKIES_SJS, { + requestCount: 1, + }); + info( + "Starting the request/response cookies in properties view copy test... " + ); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + wait = waitForDOM(document, ".headers-overview"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + clickOnSidebarTab(document, "cookies"); + + const cookiesPanel = document.querySelector("#cookies-panel"); + + const objectRows = cookiesPanel.querySelectorAll(".objectRow"); + const stringRows = cookiesPanel.querySelectorAll(".stringRow"); + + const expectedResponseCookies = [ + `{ + "__proto__": { + "httpOnly": true, + "value": "2" + } +}`, + `{ + "bob": { + "httpOnly": true, + "value": "true" + } +}`, + `{ + "foo": { + "httpOnly": true, + "value": "bar" + } +}`, + `{ + "tom": { + "httpOnly": true, + "value": "cool" + } +}`, + ]; + for (let i = 0; i < objectRows.length; i++) { + const cur = objectRows[i]; + EventUtils.sendMouseEvent({ type: "contextmenu" }, cur); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "properties-view-context-menu-copyvalue" + ); + }, expectedResponseCookies[i]); + } + + const expectedRequestCookies = ["2", "true", "bar", "cool"]; + for (let i = 0; i < expectedRequestCookies.length; i++) { + const cur = stringRows[objectRows.length + i]; + EventUtils.sendMouseEvent({ type: "contextmenu" }, cur); + await waitForClipboardPromise(async function setup() { + await selectContextMenuItem( + monitor, + "properties-view-context-menu-copyvalue" + ); + }, expectedRequestCookies[i]); + } + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_domain-not-found.js b/devtools/client/netmonitor/test/browser_net_domain-not-found.js new file mode 100644 index 0000000000..d4e3e9eaab --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_domain-not-found.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the request for a domain that is not found shows + * correctly. + */ +add_task(async function () { + const URL = "https://not-existed.com/"; + const { monitor } = await initNetMonitor(URL, { + requestCount: 1, + waitForLoad: false, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + reloadBrowser({ waitForLoad: false }); + await wait; + + const firstItem = document.querySelectorAll(".request-list-item")[0]; + + is( + firstItem.querySelector(".requests-list-url").innerText, + URL, + "The url in the displayed request is correct" + ); + is( + firstItem.querySelector(".requests-list-transferred").innerText, + "NS_ERROR_UNKNOWN_HOST", + "The error in the displayed request is correct" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_edit_resend_cancel.js b/devtools/client/netmonitor/test/browser_net_edit_resend_cancel.js new file mode 100644 index 0000000000..f2112f88e0 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_edit_resend_cancel.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if original request's header panel is visible when custom request is cancelled. + */ + +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + ok( + true, + "Skip this test when pref is true, because this panel won't be default when that is the case." + ); + return; + } + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Reload to have one request in the list + const waitForEvents = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await waitForEvents; + + // Context Menu > "Edit & Resend" + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + await waitForHeaders; + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest); + const firstRequestState = getSelectedRequest(store.getState()); + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + + // Waits for "Edit & Resend" panel to appear > New request "Cancel" + await waitUntil(() => document.querySelector(".custom-request-panel")); + document.querySelector("#custom-request-close-button").click(); + const finalRequestState = getSelectedRequest(store.getState()); + + Assert.strictEqual( + firstRequestState.id, + finalRequestState.id, + "Original request is selected after cancel button is clicked" + ); + + Assert.notStrictEqual( + document.querySelector(".headers-overview"), + null, + "Request is selected and headers panel is visible" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_edit_resend_caret.js b/devtools/client/netmonitor/test/browser_net_edit_resend_caret.js new file mode 100644 index 0000000000..c71b90b111 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_edit_resend_caret.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if position of caret does not change (resets to the end) after setting + * header's value to empty string. Also make sure the "method" field stays empty + * after removing it and resets to its original value when it looses focus. + */ + +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + ok( + true, + "Skip this test when pref is true, because this panel won't be default when that is the case." + ); + return; + } + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Reload to have one request in the list. + const waitForEvents = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await waitForEvents; + + // Open context menu and execute "Edit & Resend". + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + await waitForHeaders; + await waitForRequestData(store, ["requestHeaders"]); + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest); + + // Open "New Request" form + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + await waitUntil(() => document.querySelector("#custom-headers-value")); + const headersTextarea = document.querySelector("#custom-headers-value"); + await waitUntil(() => document.querySelector("#custom-method-value")); + const methodField = document.querySelector("#custom-method-value"); + const originalMethodValue = methodField.value; + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const request = getSelectedRequest(store.getState()); + const hostHeader = request.requestHeaders.headers[0]; + + // Close the open context menu, otherwise sendString will not work + EventUtils.synthesizeKey("VK_ESCAPE", {}); + headersTextarea.focus(); + + // Clean value of host header + const headersContent = headersTextarea.value; + const start = "Host: ".length; + const end = headersContent.indexOf("\n"); + headersTextarea.setSelectionRange(start, end); + EventUtils.synthesizeKey("VK_DELETE", {}); + + methodField.focus(); + methodField.select(); + EventUtils.synthesizeKey("VK_DELETE", {}); + + Assert.notStrictEqual( + getSelectedRequest(store.getState()).requestHeaders.headers[0], + hostHeader, + "Value of Host header was edited and should change" + ); + + ok( + headersTextarea.selectionStart === start && + headersTextarea.selectionEnd === start, + "Position of caret should not change" + ); + + Assert.strictEqual( + getSelectedRequest(store.getState()).method, + "", + "Value of method header was deleted and should be empty" + ); + + headersTextarea.focus(); + + Assert.strictEqual( + getSelectedRequest(store.getState()).method, + originalMethodValue, + "Value of method header should reset to its original value" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_edit_resend_with_filtering.js b/devtools/client/netmonitor/test/browser_net_edit_resend_with_filtering.js new file mode 100644 index 0000000000..25bb50cc8d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_edit_resend_with_filtering.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if resending a XHR request while filtering XHR displays + * the correct requests + */ +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + ok( + true, + "Skip this test when pref is true, because this panel won't be default when that is the case." + ); + return; + } + + const { tab, monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Execute XHR request and filter by XHR + await performRequests(monitor, tab, 1); + document.querySelector(".requests-list-filter-xhr-button").click(); + + // Confirm XHR request and click it + const xhrRequestItem = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, xhrRequestItem); + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + await waitForHeaders; + const firstRequest = getSelectedRequest(store.getState()); + + // Open context menu and execute "Edit & Resend". + EventUtils.sendMouseEvent({ type: "contextmenu" }, xhrRequestItem); + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + + // Wait for "Edit & Resend" panel to appear + await waitUntil(() => document.querySelector("#custom-request-send-button")); + + // Select the temporary clone-request and check its ID + // it should be calculated from the original request + // by appending '-clone' suffix. + document.querySelectorAll(".request-list-item")[1].click(); + const cloneRequest = getSelectedRequest(store.getState()); + + Assert.equal( + cloneRequest.id.replace(/-clone$/, ""), + firstRequest.id, + "The second XHR request is a clone of the first" + ); + + // Click the "Send" button and wait till the new request appears in the list + document.querySelector("#custom-request-send-button").click(); + await waitForNetworkEvents(monitor, 1); + + // Filtering by "other" so the resent request is visible after completion + document.querySelector(".requests-list-filter-other-button").click(); + + // Select the new (cloned) request + document.querySelectorAll(".request-list-item")[0].click(); + const resendRequest = getSelectedRequest(store.getState()); + + Assert.notStrictEqual( + resendRequest.id, + firstRequest.id, + "The second XHR request was made and is unique" + ); + + await teardown(monitor); +}); + +/** + * Tests if resending an XHR request while XHR filtering is on, displays + * the correct requests + */ +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + const { tab, monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const Actions = windowRequire( + "devtools/client/netmonitor/src/actions/index" + ); + store.dispatch(Actions.batchEnable(false)); + + // Execute XHR request and filter by XHR + await performRequests(monitor, tab, 1); + document.querySelector(".requests-list-filter-xhr-button").click(); + + // Confirm XHR request and click it + const xhrRequestItem = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, xhrRequestItem); + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + await waitForHeaders; + const firstRequest = getSelectedRequest(store.getState()); + + // Open context menu and execute "Edit & Resend". + EventUtils.sendMouseEvent({ type: "contextmenu" }, xhrRequestItem); + + info("Opening the new request panel"); + const waitForPanels = waitUntil( + () => + document.querySelector(".http-custom-request-panel") && + document.querySelector("#http-custom-request-send-button").disabled === + false + ); + + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + await waitForPanels; + + // Click the "Send" button and wait till the new request appears in the list + document.querySelector("#http-custom-request-send-button").click(); + await waitForNetworkEvents(monitor, 1); + + // Filtering by "other" so the resent request is visible after completion + document.querySelector(".requests-list-filter-other-button").click(); + + // Select new request + const newRequest = document.querySelectorAll(".request-list-item")[1]; + EventUtils.sendMouseEvent({ type: "mousedown" }, newRequest); + const resendRequest = getSelectedRequest(store.getState()); + + Assert.notStrictEqual( + resendRequest.id, + firstRequest.id, + "The second XHR request was made and is unique" + ); + + await teardown(monitor); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_edit_resend_xhr.js b/devtools/client/netmonitor/test/browser_net_edit_resend_xhr.js new file mode 100644 index 0000000000..e6f37d4fcc --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_edit_resend_xhr.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if editing and resending a XHR request works and the + * cloned request retains the same cause type. + */ + +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + ok( + true, + "Skip this test when pref is true, because this panel won't be default when that is the case." + ); + return; + } + + const { tab, monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Executes 1 XHR request + await performRequests(monitor, tab, 1); + + // Selects 1st XHR request + const xhrRequest = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, xhrRequest); + + // Stores original request for comparison of values later + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const original = getSelectedRequest(store.getState()); + + // Context Menu > "Edit & Resend" + EventUtils.sendMouseEvent({ type: "contextmenu" }, xhrRequest); + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + + // 1) Wait for "Edit & Resend" panel to appear + // 2) Click the "Send" button + // 3) Wait till the new request appears in the list + await waitUntil(() => document.querySelector(".custom-request-panel")); + document.querySelector("#custom-request-send-button").click(); + await waitForNetworkEvents(monitor, 1); + + // Selects cloned request + const clonedRequest = document.querySelectorAll(".request-list-item")[1]; + EventUtils.sendMouseEvent({ type: "mousedown" }, clonedRequest); + const cloned = getSelectedRequest(store.getState()); + + // Compares if the requests have the same cause type (XHR) + Assert.strictEqual( + original.cause.type, + cloned.cause.type, + "Both requests retain the same cause type" + ); + + await teardown(monitor); +}); + +/** + * Tests if editing and resending a XHR request works and the + * new request retains the same cause type. + */ + +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + const { tab, monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire( + "devtools/client/netmonitor/src/actions/index" + ); + store.dispatch(Actions.batchEnable(false)); + + // Executes 1 XHR request + await performRequests(monitor, tab, 1); + + // Selects 1st XHR request + const xhrRequest = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, xhrRequest); + + // Stores original request for comparison of values later + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const original = getSelectedRequest(store.getState()); + + // Context Menu > "Edit & Resend" + EventUtils.sendMouseEvent({ type: "contextmenu" }, xhrRequest); + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + + // 1) Wait for "Edit & Resend" panel to appear + // 2) Wait for the Send button to be enabled (i.e all the data is loaded) + // 2) Click the "Send" button + // 3) Wait till the new request appears in the list + await waitUntil( + () => + document.querySelector(".http-custom-request-panel") && + document.querySelector("#http-custom-request-send-button").disabled === + false + ); + document.querySelector("#http-custom-request-send-button").click(); + await waitForNetworkEvents(monitor, 1); + + // Selects new request + const newRequest = document.querySelectorAll(".request-list-item")[1]; + EventUtils.sendMouseEvent({ type: "mousedown" }, newRequest); + const request = getSelectedRequest(store.getState()); + + Assert.strictEqual( + original.cause.type, + request.cause.type, + "Both requests retain the same cause type" + ); + + await teardown(monitor); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_error-boundary-01.js b/devtools/client/netmonitor/test/browser_net_error-boundary-01.js new file mode 100644 index 0000000000..cb27428d3f --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_error-boundary-01.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test that top-level net monitor error boundary catches child errors. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + + const { store, windowRequire, document } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Intentionally damage the store to cause a child component error + const state = store.getState(); + state.ui.columns = null; + + await reloadBrowser(); + + // Wait for the panel to fall back to the error UI + const errorPanel = await waitUntil(() => + document.querySelector(".app-error-panel") + ); + + is(errorPanel, !undefined); + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_filter-01.js b/devtools/client/netmonitor/test/browser_net_filter-01.js new file mode 100644 index 0000000000..f72fb4d8c5 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_filter-01.js @@ -0,0 +1,558 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Used to test filtering a unicode URI component +const UNICODE_IN_URI_COMPONENT = "\u6e2c"; +const ENCODED_CHARS_IN_URI_COMP = encodeURIComponent(UNICODE_IN_URI_COMPONENT); + +// Used to test filtering an international domain name with Unicode +const IDN = "xn--hxajbheg2az3al.xn--jxalpdlp"; +const UNICODE_IN_IDN = "\u03c0\u03b1"; + +/** + * Test if filtering items in the network table works correctly. + */ +const BASIC_REQUESTS = [ + { + url: getSjsURLInUnicodeIdn() + "?fmt=html&res=undefined&text=Sample&cors=1", + }, + { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" }, + { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" }, + { + url: `sjs_content-type-test-server.sjs?fmt=html&text=${ENCODED_CHARS_IN_URI_COMP}`, + }, + { + url: `sjs_content-type-test-server.sjs?fmt=css&text=${ENCODED_CHARS_IN_URI_COMP}`, + }, + { + url: `sjs_content-type-test-server.sjs?fmt=js&text=${ENCODED_CHARS_IN_URI_COMP}`, + }, +]; + +const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([ + { url: getSjsURLInUnicodeIdn() + "?fmt=font&cors=1" }, + { url: "sjs_content-type-test-server.sjs?fmt=image" }, + { url: "sjs_content-type-test-server.sjs?fmt=audio" }, + { url: "sjs_content-type-test-server.sjs?fmt=application-ogg" }, + { url: "sjs_content-type-test-server.sjs?fmt=video" }, + { url: "sjs_content-type-test-server.sjs?fmt=hls-m3u8" }, + { url: "sjs_content-type-test-server.sjs?fmt=hls-m3u8-alt-mime-type" }, +]); + +const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([ + { url: "sjs_content-type-test-server.sjs?fmt=flash" }, +]); + +const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = + REQUESTS_WITH_MEDIA_AND_FLASH.concat([ + /* Use new WebSocket() to mock native websocket request, then "Upgrade" will be added */ + { url: WS_URL + "sjs_content-type-test-server.sjs?fmt=ws", ws: true }, + ]); + +const EXPECTED_REQUESTS = [ + { + method: "GET", + url: getSjsURLInUnicodeIdn() + "?fmt=html&res=undefined&text=Sample&cors=1", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=css&text=sample", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "css", + fullMimeType: "text/css; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=js&text=sample", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "js", + fullMimeType: "application/javascript; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + `?fmt=html&text=${ENCODED_CHARS_IN_URI_COMP}`, + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + `?fmt=css&text=${ENCODED_CHARS_IN_URI_COMP}`, + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "css", + fullMimeType: "text/css; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + `?fmt=js&text=${ENCODED_CHARS_IN_URI_COMP}`, + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "js", + fullMimeType: "application/javascript; charset=utf-8", + }, + }, + { + method: "GET", + url: getSjsURLInUnicodeIdn() + "?fmt=font&cors=1", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "woff", + fullMimeType: "font/woff", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=image", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "png", + fullMimeType: "image/png", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=audio", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "ogg", + fullMimeType: "audio/ogg", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=application-ogg", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "ogg", + fullMimeType: "application/ogg", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=video", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "webm", + fullMimeType: "video/webm", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=hls-m3u8", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "x-mpegurl", + fullMimeType: "application/x-mpegurl", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=hls-m3u8-alt-mime-type", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "vnd.apple.mpegurl", + fullMimeType: "application/vnd.apple.mpegurl", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=flash", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "x-shockwave-flash", + fullMimeType: "application/x-shockwave-flash", + }, + }, + { + method: "GET", + url: WS_WS_CONTENT_TYPE_SJS + "?fmt=ws", + data: { + fuzzyUrl: true, + status: 101, + statusText: "Switching Protocols", + }, + }, +]; + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSelectedRequest, getSortedRequests } = + windowRequire("devtools/client/netmonitor/src/selectors/index"); + + store.dispatch(Actions.batchEnable(false)); + + function setFreetextFilter(value) { + store.dispatch(Actions.setRequestFilterText(value)); + } + + info("Starting test... "); + + const wait = waitForNetworkEvents( + monitor, + REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS.length + ); + await performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS); + await wait; + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + + isnot( + getSelectedRequest(store.getState()), + null, + "There should be a selected item in the requests menu." + ); + is( + getSelectedIndex(store.getState()), + 0, + "The first item should be selected in the requests menu." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should render correctly." + ); + + // First test with single filters... + testFilterButtons(monitor, "all"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + testFilterButtons(monitor, "html"); + await testContents([1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + // Reset filters + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-css-button") + ); + testFilterButtons(monitor, "css"); + await testContents([0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-js-button") + ); + testFilterButtons(monitor, "js"); + await testContents([0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-xhr-button") + ); + testFilterButtons(monitor, "xhr"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-fonts-button") + ); + testFilterButtons(monitor, "fonts"); + await testContents([0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-images-button") + ); + testFilterButtons(monitor, "images"); + await testContents([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-media-button") + ); + testFilterButtons(monitor, "media"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-ws-button") + ); + testFilterButtons(monitor, "ws"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + + testFilterButtons(monitor, "all"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + // Text in filter box that matches nothing should hide all. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter("foobar"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + // ASCII text in filter box that matches should filter out everything else. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter("sample"); + await testContents([1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + // ASCII text in filter box that matches should filter out everything else. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter("SAMPLE"); + await testContents([1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + // Test negative filtering ASCII text(only show unmatched items) + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter("-sample"); + await testContents([0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + // Unicode text in filter box that matches should filter out everything else. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter(UNICODE_IN_URI_COMPONENT); + await testContents([0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + // Ditto, except the above is for a Unicode URI component, and this one is for + // a Unicode domain name. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter(UNICODE_IN_IDN); + await testContents([1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + // Test negative filtering Unicode text(only show unmatched items) + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter(`-${UNICODE_IN_URI_COMPONENT}`); + await testContents([1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + // Ditto, except the above is for a Unicode URI component, and this one is for + // a Unicode domain name. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter(`-${UNICODE_IN_IDN}`); + await testContents([0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1]); + + // ...then combine multiple filters together. + + // Enable filtering for html and css; should show request of both type. + setFreetextFilter(""); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-css-button") + ); + testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 0, 0]); + await testContents([1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + // Html and css filter enabled and text filter should show just the html and css match. + // Should not show both the items matching the button plus the items matching the text. + setFreetextFilter("sample"); + await testContents([1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + setFreetextFilter(UNICODE_IN_URI_COMPONENT); + await testContents([0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + setFreetextFilter(""); + testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 0, 0]); + await testContents([1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + // Disable some filters. Only one left active. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-css-button") + ); + testFilterButtons(monitor, "html"); + await testContents([1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + // Disable last active filter. Should toggle to all. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + testFilterButtons(monitor, "all"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + // Enable few filters and click on all. Only "all" should be checked. + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-css-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-ws-button") + ); + testFilterButtonsCustom(monitor, [0, 1, 1, 0, 0, 0, 0, 0, 1, 0]); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + testFilterButtons(monitor, "all"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + await teardown(monitor); + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } + + async function testContents(visibility) { + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + } + + const items = getSortedRequests(store.getState()); + let visibleItems; + + // Filter results will be updated asynchronously, so we should wait until + // displayed requests reach final state. + await waitUntil(() => { + visibleItems = getDisplayedRequests(store.getState()); + return visibleItems.length === visibility.filter(e => e).length; + }); + + is( + items.length, + visibility.length, + "There should be a specific amount of items in the requests menu." + ); + is( + visibleItems.length, + visibility.filter(e => e).length, + "There should be a specific amount of visible items in the requests menu." + ); + + for (let i = 0; i < visibility.length; i++) { + const itemId = items[i].id; + const shouldBeVisible = !!visibility[i]; + const isThere = visibleItems.some(r => r.id == itemId); + + is( + isThere, + shouldBeVisible, + `The item at index ${i} has visibility=${shouldBeVisible}` + ); + + if (shouldBeVisible) { + const { method, url, data } = EXPECTED_REQUESTS[i]; + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[i], + method, + url, + data + ); + } + } + } +}); + +function getSjsURLInUnicodeIdn() { + const { hostname } = new URL(CONTENT_TYPE_SJS); + return CONTENT_TYPE_SJS.replace(hostname, IDN); +} diff --git a/devtools/client/netmonitor/test/browser_net_filter-02.js b/devtools/client/netmonitor/test/browser_net_filter-02.js new file mode 100644 index 0000000000..2e2b7752da --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_filter-02.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if filtering items in the network table works correctly with new requests. + */ + +const BASIC_REQUESTS = [ + { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" }, + { url: "sjs_content-type-test-server.sjs?fmt=xhtml" }, + { url: "sjs_content-type-test-server.sjs?fmt=css" }, + { url: "sjs_content-type-test-server.sjs?fmt=js" }, +]; + +const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([ + { url: "sjs_content-type-test-server.sjs?fmt=font" }, + { url: "sjs_content-type-test-server.sjs?fmt=image" }, + { url: "sjs_content-type-test-server.sjs?fmt=audio" }, + { url: "sjs_content-type-test-server.sjs?fmt=video" }, +]); + +const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([ + { url: "sjs_content-type-test-server.sjs?fmt=flash" }, +]); + +const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = + REQUESTS_WITH_MEDIA_AND_FLASH.concat([ + /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */ + { url: "sjs_content-type-test-server.sjs?fmt=ws" }, + ]); + +const EXPECTED_REQUESTS = [ + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=html", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=xhtml", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "xhtml", + fullMimeType: "application/xhtml+xml; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=css", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "css", + fullMimeType: "text/css; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=js", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "js", + fullMimeType: "application/javascript; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=font", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "woff", + fullMimeType: "font/woff", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=image", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "png", + fullMimeType: "image/png", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=audio", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "ogg", + fullMimeType: "audio/ogg", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=video", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "webm", + fullMimeType: "video/webm", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=flash", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "x-shockwave-flash", + fullMimeType: "application/x-shockwave-flash", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=ws", + data: { + fuzzyUrl: true, + status: 101, + statusText: "Switching Protocols", + }, + }, +]; + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + info("Starting test... "); + + // It seems that this test may be slow on Ubuntu builds running on ec2. + requestLongerTimeout(2); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSelectedRequest, getSortedRequests } = + windowRequire("devtools/client/netmonitor/src/selectors/index"); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 10); + await performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS); + await wait; + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + + isnot( + getSelectedRequest(store.getState()), + null, + "There should be a selected item in the requests menu." + ); + is( + getSelectedIndex(store.getState()), + 0, + "The first item should be selected in the requests menu." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should be visible after toggle button was pressed." + ); + + testFilterButtons(monitor, "all"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + info("Testing html filtering."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + testFilterButtons(monitor, "html"); + await testContents([1, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + info("Performing more requests."); + // As the view is filtered and there is only one request for which we fetch event timings + wait = waitForNetworkEvents(monitor, 10, { expectedEventTimings: 1 }); + await performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS); + await wait; + + info("Testing html filtering again."); + testFilterButtons(monitor, "html"); + await testContents([ + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + + info("Performing more requests."); + wait = waitForNetworkEvents(monitor, 10, { expectedEventTimings: 1 }); + await performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS); + await wait; + + info("Testing html filtering again."); + testFilterButtons(monitor, "html"); + await testContents([ + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, + ]); + + info("Resetting filters."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + testFilterButtons(monitor, "all"); + await testContents([ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + ]); + + await teardown(monitor); + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } + + async function testContents(visibility) { + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + } + + isnot( + getSelectedRequest(store.getState()), + null, + "There should still be a selected item after filtering." + ); + is( + getSelectedIndex(store.getState()), + 0, + "The first item should be still selected after filtering." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should still be visible after filtering." + ); + + const items = getSortedRequests(store.getState()); + const visibleItems = getDisplayedRequests(store.getState()); + + is( + items.length, + visibility.length, + "There should be a specific amount of items in the requests menu." + ); + is( + visibleItems.length, + visibility.filter(e => e).length, + "There should be a specific amount of visible items in the requests menu." + ); + + for (let i = 0; i < visibility.length; i++) { + const itemId = items[i].id; + const shouldBeVisible = !!visibility[i]; + const isThere = visibleItems.some(r => r.id == itemId); + is( + isThere, + shouldBeVisible, + `The item at index ${i} has visibility=${shouldBeVisible}` + ); + } + + for (let i = 0; i < EXPECTED_REQUESTS.length; i++) { + const { method, url, data } = EXPECTED_REQUESTS[i]; + for (let j = i; j < visibility.length; j += EXPECTED_REQUESTS.length) { + if (visibility[j]) { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[i], + method, + url, + data + ); + } + } + } + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_filter-03.js b/devtools/client/netmonitor/test/browser_net_filter-03.js new file mode 100644 index 0000000000..b7ce43871e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_filter-03.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if filtering items in the network table works correctly with new requests + * and while sorting is enabled. + */ +const BASIC_REQUESTS = [ + { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" }, + { url: "sjs_content-type-test-server.sjs?fmt=css" }, + { url: "sjs_content-type-test-server.sjs?fmt=js" }, +]; + +const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([ + { url: "sjs_content-type-test-server.sjs?fmt=font" }, + { url: "sjs_content-type-test-server.sjs?fmt=image" }, + { url: "sjs_content-type-test-server.sjs?fmt=audio" }, + { url: "sjs_content-type-test-server.sjs?fmt=video" }, +]); + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + info("Starting test... "); + + // It seems that this test may be slow on Ubuntu builds running on ec2. + requestLongerTimeout(2); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSelectedRequest, getSortedRequests } = + windowRequire("devtools/client/netmonitor/src/selectors/index"); + + store.dispatch(Actions.batchEnable(false)); + + // The test assumes that the first HTML request here has a longer response + // body than the other HTML requests performed later during the test. + const requests = Cu.cloneInto(REQUESTS_WITH_MEDIA, {}); + const newres = "res=<p>" + new Array(10).join(Math.random(10)) + "</p>"; + requests[0].url = requests[0].url.replace("res=undefined", newres); + + let wait = waitForNetworkEvents(monitor, 7); + await performRequestsInContent(requests); + await wait; + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + + isnot( + getSelectedRequest(store.getState()), + null, + "There should be a selected item in the requests menu." + ); + is( + getSelectedIndex(store.getState()), + 0, + "The first item should be selected in the requests menu." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should be visible after toggle button was pressed." + ); + + testFilterButtons(monitor, "all"); + testContents([0, 1, 2, 3, 4, 5, 6], 7, 0); + + info("Sorting by size, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-contentSize-button") + ); + testFilterButtons(monitor, "all"); + testContents([6, 4, 5, 0, 1, 2, 3], 7, 6); + + info("Testing html filtering."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + testFilterButtons(monitor, "html"); + testContents([6, 4, 5, 0, 1, 2, 3], 1, 6); + + info("Performing more requests."); + // As the view is filtered and there is only one request for which we fetch event timings + wait = waitForNetworkEvents(monitor, 7, { expectedEventTimings: 1 }); + performRequestsInContent(REQUESTS_WITH_MEDIA); + await wait; + + info("Testing html filtering again."); + resetSorting(); + testFilterButtons(monitor, "html"); + testContents([8, 13, 9, 11, 10, 12, 0, 4, 1, 5, 2, 6, 3, 7], 2, 13); + + info("Performing more requests."); + // As the view is filtered and there is only one request for which we fetch event timings + wait = waitForNetworkEvents(monitor, 7, { expectedEventTimings: 1 }); + performRequestsInContent(REQUESTS_WITH_MEDIA); + await wait; + + info("Testing html filtering again."); + resetSorting(); + testFilterButtons(monitor, "html"); + testContents( + [12, 13, 20, 14, 16, 18, 15, 17, 19, 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11], + 3, + 20 + ); + + await teardown(monitor); + + function resetSorting() { + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-initiator-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-contentSize-button") + ); + } + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } + + function testContents(order, visible, selection) { + isnot( + getSelectedRequest(store.getState()), + null, + "There should still be a selected item after filtering." + ); + is( + getSelectedIndex(store.getState()), + selection, + "The first item should be still selected after filtering." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should still be visible after filtering." + ); + + is( + getSortedRequests(store.getState()).length, + order.length, + "There should be a specific amount of items in the requests menu." + ); + is( + getDisplayedRequests(store.getState()).length, + visible, + "There should be a specific amount of visible items in the requests menu." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_filter-04.js b/devtools/client/netmonitor/test/browser_net_filter-04.js new file mode 100644 index 0000000000..d690813285 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_filter-04.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if invalid filter types are sanitized when loaded from the preferences. + */ + +const BASIC_REQUESTS = [ + { url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined" }, + { url: "sjs_content-type-test-server.sjs?fmt=css" }, + { url: "sjs_content-type-test-server.sjs?fmt=js" }, +]; + +const REQUESTS_WITH_MEDIA = BASIC_REQUESTS.concat([ + { url: "sjs_content-type-test-server.sjs?fmt=font" }, + { url: "sjs_content-type-test-server.sjs?fmt=image" }, + { url: "sjs_content-type-test-server.sjs?fmt=audio" }, + { url: "sjs_content-type-test-server.sjs?fmt=video" }, +]); + +const REQUESTS_WITH_MEDIA_AND_FLASH = REQUESTS_WITH_MEDIA.concat([ + { url: "sjs_content-type-test-server.sjs?fmt=flash" }, +]); + +const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = + REQUESTS_WITH_MEDIA_AND_FLASH.concat([ + /* "Upgrade" is a reserved header and can not be set on XMLHttpRequest */ + { url: "sjs_content-type-test-server.sjs?fmt=ws" }, + ]); + +add_task(async function () { + Services.prefs.setCharPref( + "devtools.netmonitor.filters", + '["bogus", "js", "alsobogus"]' + ); + + const { monitor } = await initNetMonitor(FILTERING_URL, { + requestCount: 1, + expectedEventTimings: 0, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { Prefs } = windowRequire("devtools/client/netmonitor/src/utils/prefs"); + + store.dispatch(Actions.batchEnable(false)); + + is(Prefs.filters.length, 3, "All the filter types should be loaded."); + is( + Prefs.filters[0], + "bogus", + "The first filter type is invalid, but loaded anyway." + ); + + // As the view is filtered and there is only one request for which we fetch event timings + const wait = waitForNetworkEvents(monitor, 9, { expectedEventTimings: 1 }); + await performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS); + await wait; + + testFilterButtons(monitor, "js"); + ok(true, "Only the correct filter type was taken into consideration."); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + + const filters = Services.prefs.getCharPref("devtools.netmonitor.filters"); + is( + filters, + '["html","js"]', + "The filters preferences were saved directly after the click and only" + + " with the valid." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_filter-autocomplete.js b/devtools/client/netmonitor/test/browser_net_filter-autocomplete.js new file mode 100644 index 0000000000..974c18a4f2 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_filter-autocomplete.js @@ -0,0 +1,207 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test autocomplete based on filtering flags and requests + */ +const REQUESTS = [ + { + url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample", + }, + { + url: + "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample" + + "&cookies=1", + }, + { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" }, + { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" }, + { url: "sjs_content-type-test-server.sjs?fmt=font" }, + { url: "sjs_content-type-test-server.sjs?fmt=image" }, + { url: "sjs_content-type-test-server.sjs?fmt=audio" }, + { url: "sjs_content-type-test-server.sjs?fmt=video" }, + { url: "sjs_content-type-test-server.sjs?fmt=gzip" }, + { url: "sjs_status-codes-test-server.sjs?sts=304" }, +]; + +function testAutocompleteContents(expected, document) { + expected.forEach(function (item, i) { + is( + document.querySelector( + `.devtools-autocomplete-listbox .autocomplete-item:nth-child(${i + 1})` + ).textContent, + item, + `${expected[i]} found` + ); + }); +} + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Starting test... "); + + // Let the requests load completely before the autocomplete tests begin + // as autocomplete values also rely on the network requests. + const waitNetwork = waitForNetworkEvents(monitor, REQUESTS.length); + await performRequestsInContent(REQUESTS); + await waitNetwork; + + EventUtils.synthesizeMouseAtCenter( + document.querySelector(".devtools-filterinput"), + {}, + document.defaultView + ); + // Empty Mouse click should keep autocomplete hidden + ok( + !document.querySelector(".devtools-autocomplete-popup"), + "Autocomplete Popup still hidden" + ); + + document.querySelector(".devtools-filterinput").focus(); + + // Typing numbers that corresponds to status codes should invoke an autocomplete + EventUtils.sendString("2"); + ok( + document.querySelector(".devtools-autocomplete-popup"), + "Autocomplete Popup Created" + ); + testAutocompleteContents( + [ + "status-code:200", + "status-code:201", + "status-code:202", + "status-code:203", + "status-code:204", + "status-code:205", + "status-code:206", + ], + document + ); + EventUtils.synthesizeKey("KEY_Enter"); + is( + document.querySelector(".devtools-filterinput").value, + "status-code:200", + "Value correctly set after Enter" + ); + ok( + !document.querySelector(".devtools-autocomplete-popup"), + "Autocomplete Popup hidden after keyboard Enter key" + ); + + // Space separated tokens + // The last token where autocomplete is available shall generate the popup + EventUtils.sendString(" s"); + ok( + document.querySelector(".devtools-autocomplete-popup"), + "Autocomplete Popup Created" + ); + testAutocompleteContents( + [ + "scheme:", + "set-cookie-domain:", + "set-cookie-name:", + "set-cookie-value:", + "size:", + "status-code:", + ], + document + ); + + EventUtils.sendString("c"); + testAutocompleteContents(["scheme:"], document); + EventUtils.synthesizeKey("KEY_Tab"); + // Tab selection should hide autocomplete + ok( + document.querySelector(".devtools-autocomplete-popup"), + "Autocomplete Popup alive with content values" + ); + testAutocompleteContents(["scheme:http"], document); + + EventUtils.synthesizeKey("KEY_Enter"); + is( + document.querySelector(".devtools-filterinput").value, + "status-code:200 scheme:http", + "Value correctly set after Enter" + ); + ok( + !document.querySelector(".devtools-autocomplete-popup"), + "Autocomplete Popup hidden after keyboard Enter key" + ); + + // Space separated tokens + // The last token where autocomplete is available shall generate the popup + EventUtils.sendString(" pro"); + testAutocompleteContents(["protocol:"], document); + + // The new value of the text box should be previousTokens + latest value selected + // First return selects "protocol:" + EventUtils.synthesizeKey("KEY_Enter"); + // Second return selects "protocol:HTTP/1.1" + EventUtils.synthesizeKey("KEY_Enter"); + is( + document.querySelector(".devtools-filterinput").value, + "status-code:200 scheme:http protocol:HTTP/1.1", + "Tokenized click generates correct value in input box" + ); + + // Explicitly type in `flag:` renders autocomplete with values + EventUtils.sendString(" status-code:"); + testAutocompleteContents(["status-code:200", "status-code:304"], document); + + // Typing the exact value closes autocomplete + EventUtils.sendString("304"); + ok( + !document.querySelector(".devtools-autocomplete-popup"), + "Typing the exact value closes autocomplete" + ); + + // Check if mime-type has been correctly parsed out and values also get autocomplete + EventUtils.sendString(" mime-type:text"); + testAutocompleteContents( + ["mime-type:text/css", "mime-type:text/html", "mime-type:text/plain"], + document + ); + + // The negative filter flags + EventUtils.sendString(" -"); + testAutocompleteContents( + [ + "-cause:", + "-domain:", + "-has-response-header:", + "-initiator:", + "-is:", + "-larger-than:", + "-method:", + "-mime-type:", + "-priority:", + "-protocol:", + "-regexp:", + "-remote-ip:", + "-scheme:", + "-set-cookie-domain:", + "-set-cookie-name:", + "-set-cookie-value:", + "-size:", + "-status-code:", + "-transferred-larger-than:", + "-transferred:", + ], + document + ); + + // Autocomplete for negative filtering + EventUtils.sendString("is:"); + testAutocompleteContents( + ["-is:cached", "-is:from-cache", "-is:running"], + document + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_filter-flags.js b/devtools/client/netmonitor/test/browser_net_filter-flags.js new file mode 100644 index 0000000000..f3007b6bf5 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_filter-flags.js @@ -0,0 +1,435 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(3); + +/** + * Test different text filtering flags + */ +const REQUESTS = [ + { + url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample", + }, + { + url: + "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample" + + "&cookies=1", + }, + { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" }, + { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" }, + { url: "sjs_content-type-test-server.sjs?fmt=font" }, + { url: "sjs_content-type-test-server.sjs?fmt=image" }, + { url: "sjs_content-type-test-server.sjs?fmt=audio" }, + { url: "sjs_content-type-test-server.sjs?fmt=video" }, + { url: "sjs_content-type-test-server.sjs?fmt=gzip" }, + { url: "sjs_status-codes-test-server.sjs?sts=304" }, +]; + +const EXPECTED_REQUESTS = [ + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=html", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=html", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "html", + fullMimeType: "text/html; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=css", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "css", + fullMimeType: "text/css; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=js", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "js", + fullMimeType: "application/javascript; charset=utf-8", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=font", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "woff", + fullMimeType: "font/woff", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=image", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "png", + fullMimeType: "image/png", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=audio", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "ogg", + fullMimeType: "audio/ogg", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=video", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + type: "webm", + fullMimeType: "video/webm", + }, + }, + { + method: "GET", + url: CONTENT_TYPE_SJS + "?fmt=gzip", + data: { + fuzzyUrl: true, + status: 200, + statusText: "OK", + displayedStatus: "200", + type: "plain", + fullMimeType: "text/plain", + }, + }, + { + method: "GET", + url: STATUS_CODES_SJS + "?sts=304", + data: { + status: 304, + statusText: "Not Modified", + displayedStatus: "304", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + }, + }, +]; + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Filtering network request will start fetching data lazily + // (fetching requestHeaders & responseHeaders for filtering WS & XHR) + // Lazy fetching will be executed when user focuses on filter box. + function setFreetextFilter(value) { + const filterBox = document.querySelector(".devtools-filterinput"); + filterBox.focus(); + filterBox.value = ""; + typeInNetmonitor(value, monitor); + } + + info("Starting test... "); + + const waitNetwork = waitForNetworkEvents(monitor, REQUESTS.length); + await performRequestsInContent(REQUESTS); + await waitNetwork; + + info(" > Test running flag once requests finish running"); + setFreetextFilter("is:running"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test cached flag"); + setFreetextFilter("is:from-cache"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + setFreetextFilter("is:cached"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + info(" > Test negative cached flag"); + setFreetextFilter("-is:from-cache"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 0]); + + setFreetextFilter("-is:cached"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 0]); + + info(" > Test status-code flag"); + setFreetextFilter("status-code:200"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 0]); + + info(" > Test status-code negative flag"); + setFreetextFilter("-status-code:200"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + info(" > Test mime-type flag"); + setFreetextFilter("mime-type:HtmL"); + await testContents([1, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test mime-type negative flag"); + setFreetextFilter("-mime-type:HtmL"); + await testContents([0, 0, 1, 1, 1, 1, 1, 1, 1, 1]); + + info(" > Test method flag"); + setFreetextFilter("method:get"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + info(" > Test unmatched method flag"); + setFreetextFilter("method:post"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test scheme flag (all requests are http)"); + setFreetextFilter("scheme:http"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + setFreetextFilter("scheme:https"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test regex filter"); + setFreetextFilter("regexp:content.*?Sam"); + await testContents([1, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test set-cookie-name flag"); + setFreetextFilter("set-cookie-name:name2"); + await testContents([0, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + setFreetextFilter("set-cookie-name:not-existing"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test set-cookie-value flag"); + setFreetextFilter("set-cookie-value:value2"); + await testContents([0, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + setFreetextFilter("set-cookie-value:not-existing"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test set-cookie-domain flag"); + setFreetextFilter("set-cookie-domain:.example.com"); + await testContents([0, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + setFreetextFilter("set-cookie-domain:.foo.example.com"); + await testContents([0, 1, 0, 0, 0, 0, 0, 0, 0, 0]); + + setFreetextFilter("set-cookie-domain:.not-existing.example.com"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test size"); + setFreetextFilter("size:-1"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + setFreetextFilter("size:0"); + await testContents([0, 0, 0, 0, 1, 1, 1, 1, 0, 1]); + + setFreetextFilter("size:34"); + await testContents([0, 0, 1, 1, 0, 0, 0, 0, 0, 0]); + + setFreetextFilter("size:0kb"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Testing the lower bound"); + setFreetextFilter("size:9.659k"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Testing the actual value"); + setFreetextFilter("size:10989"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 1, 0]); + + info(" > Testing the upper bound"); + setFreetextFilter("size:11.804k"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 1, 0]); + + info(" > Test transferred"); + setFreetextFilter("transferred:200"); + await testContents([0, 0, 0, 0, 1, 1, 1, 1, 0, 0]); + + setFreetextFilter("transferred:234"); + await testContents([1, 0, 1, 0, 0, 0, 0, 0, 0, 1]); + + setFreetextFilter("transferred:248"); + await testContents([0, 0, 1, 1, 0, 0, 0, 0, 0, 1]); + + setFreetextFilter("transferred:0kb"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test larger-than"); + setFreetextFilter("larger-than:-1"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + setFreetextFilter("larger-than:0"); + await testContents([1, 1, 1, 1, 0, 0, 0, 0, 1, 0]); + + setFreetextFilter("larger-than:33"); + await testContents([0, 0, 1, 1, 0, 0, 0, 0, 1, 0]); + + setFreetextFilter("larger-than:34"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 1, 0]); + + setFreetextFilter("larger-than:10.73k"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 1, 0]); + + setFreetextFilter("larger-than:10.732k"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 1, 0]); + + setFreetextFilter("larger-than:0kb"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test transferred-larger-than"); + setFreetextFilter("transferred-larger-than:-1"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + setFreetextFilter("transferred-larger-than:214"); + await testContents([1, 1, 1, 1, 0, 0, 0, 0, 1, 1]); + + setFreetextFilter("transferred-larger-than:247"); + await testContents([0, 1, 1, 1, 0, 0, 0, 0, 1, 0]); + + setFreetextFilter("transferred-larger-than:248"); + await testContents([0, 1, 0, 1, 0, 0, 0, 0, 1, 0]); + + setFreetextFilter("transferred-larger-than:10.73k"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + setFreetextFilter("transferred-larger-than:0kb"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test cause"); + setFreetextFilter("cause:xhr"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + setFreetextFilter("cause:script"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test has-response-header"); + setFreetextFilter("has-response-header:Content-Type"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + setFreetextFilter("has-response-header:Last-Modified"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test remote-ip"); + setFreetextFilter("remote-ip:127.0.0.1"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + setFreetextFilter("remote-ip:192.168.1.2"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test domain"); + setFreetextFilter("domain:example.com"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + setFreetextFilter("domain:wrongexample.com"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test protocol"); + setFreetextFilter("protocol:http/1"); + await testContents([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); + + setFreetextFilter("protocol:http/2"); + await testContents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + info(" > Test mixing flags"); + setFreetextFilter("-mime-type:HtmL status-code:200"); + await testContents([0, 0, 1, 1, 1, 1, 1, 1, 1, 0]); + + await teardown(monitor); + + async function testContents(visibility) { + const items = getSortedRequests(store.getState()); + let visibleItems = getDisplayedRequests(store.getState()); + + // Filter results will be updated asynchronously, so we should wait until + // displayed requests reach final state. + await waitUntil(() => { + visibleItems = getDisplayedRequests(store.getState()); + return visibleItems.length === visibility.filter(e => e).length; + }); + + is( + items.length, + visibility.length, + "There should be a specific amount of items in the requests menu." + ); + is( + visibleItems.length, + visibility.filter(e => e).length, + "There should be a specific amount of visible items in the requests menu." + ); + + for (let i = 0; i < visibility.length; i++) { + const itemId = items[i].id; + const shouldBeVisible = !!visibility[i]; + let isThere = visibleItems.some(r => r.id == itemId); + + // Filter results will be updated asynchronously, so we should wait until + // displayed requests reach final state. + await waitUntil(() => { + visibleItems = getDisplayedRequests(store.getState()); + isThere = visibleItems.some(r => r.id == itemId); + return isThere === shouldBeVisible; + }); + + is( + isThere, + shouldBeVisible, + `The item at index ${i} has visibility=${shouldBeVisible}` + ); + } + + // Fake mouse over the status column only after the list is fully updated + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + } + + for (let i = 0; i < visibility.length; i++) { + const shouldBeVisible = !!visibility[i]; + + if (shouldBeVisible) { + const { method, url, data } = EXPECTED_REQUESTS[i]; + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[i], + method, + url, + data + ); + } + } + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_filter-sts-search.js b/devtools/client/netmonitor/test/browser_net_filter-sts-search.js new file mode 100644 index 0000000000..d8de156cb6 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_filter-sts-search.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test incomplete status-code search + */ +const REQUESTS = [ + { url: "sjs_status-codes-test-server.sjs?sts=400" }, + { url: "sjs_status-codes-test-server.sjs?sts=300" }, + { url: "sjs_status-codes-test-server.sjs?sts=304" }, +]; + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + const { getDisplayedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + info("Starting test... "); + + // Let the requests load completely before the incomplete search tests begin + // because we are searching for the status code in these requests. + const waitNetwork = waitForNetworkEvents(monitor, REQUESTS.length); + await performRequestsInContent(REQUESTS); + await waitNetwork; + + document.querySelector(".devtools-filterinput").focus(); + + EventUtils.sendString("status-code:3"); + + let visibleItems = getDisplayedRequests(store.getState()); + + // Results will be updated asynchronously, so we should wait until + // displayed requests reach final state. + await waitUntil(() => { + visibleItems = getDisplayedRequests(store.getState()); + return visibleItems.length === 2; + }); + + is( + Number(visibleItems[0].status), + 303, + "First visible item has expected status" + ); + is( + Number(visibleItems[1].status), + 304, + "Second visible item has expected status" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_filter-value-preserved.js b/devtools/client/netmonitor/test/browser_net_filter-value-preserved.js new file mode 100644 index 0000000000..6e022e9830 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_filter-value-preserved.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that filter input keeps its value when host or panel changes + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Starting test... "); + + const toolbars = document.querySelector("#netmonitor-toolbar-container"); + let input = toolbars.querySelector(".devtools-filterinput"); + input.value = "hello"; + + await monitor.toolbox.switchHost("right"); + await waitFor( + () => toolbars.querySelectorAll(".devtools-toolbar").length == 2 + ); + + is( + toolbars.querySelectorAll(".devtools-toolbar").length, + 2, + "Should be in 2 toolbar mode" + ); + + input = toolbars.querySelector(".devtools-filterinput"); + is( + input.value, + "hello", + "Value should be preserved after switching to right host" + ); + + await monitor.toolbox.switchHost("bottom"); + + input = toolbars.querySelector(".devtools-filterinput"); + is( + input.value, + "hello", + "Value should be preserved after switching to bottom host" + ); + + await monitor.toolbox.selectTool("inspector"); + await monitor.toolbox.selectTool("netmonitor"); + + input = toolbars.querySelector(".devtools-filterinput"); + is(input.value, "hello", "Value should be preserved after switching tools"); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_fission_switch_target.js b/devtools/client/netmonitor/test/browser_net_fission_switch_target.js new file mode 100644 index 0000000000..a34e17c02e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_fission_switch_target.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test switching for the top-level target. + +const EXAMPLE_COM_URL = "https://example.com/document-builder.sjs?html=testcom"; +const EXAMPLE_NET_URL = "https://example.net/document-builder.sjs?html=testnet"; +const REQUEST_URL = HTTPS_SEARCH_SJS + "?value=test"; +const PARENT_PROCESS_URL = "about:blank"; + +add_task(async function () { + info("Open a page that runs on the content process and the netmonitor"); + const { monitor } = await initNetMonitor(EXAMPLE_COM_URL, { + requestCount: 1, + }); + await assertRequest(monitor, REQUEST_URL); + + info("Navigate to a page that runs in another content process (if fission)"); + await waitForUpdatesAndNavigateTo(EXAMPLE_NET_URL); + await assertRequest(monitor, REQUEST_URL); + + info("Navigate to a parent process page"); + await waitForUpdatesAndNavigateTo(PARENT_PROCESS_URL); + await assertRequest(monitor, REQUEST_URL); + + info("Navigate back to the example.com content page"); + await waitForUpdatesAndNavigateTo(EXAMPLE_COM_URL); + await assertRequest(monitor, REQUEST_URL); + + await teardown(monitor); +}); + +async function waitForUpdatesAndNavigateTo(url) { + await waitForAllNetworkUpdateEvents(); + await navigateTo(url); +} + +async function assertRequest(monitor, url) { + const waitForRequests = waitForNetworkEvents(monitor, 1); + info("Create a request in the target page for the URL: " + url); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [url], async _url => { + // Note: we are not reusing performRequests available in some netmonitor + // test pages because we also need to run this helper against an about: + // page, which won't have the helper defined. + // Therefore, we use a simplified performRequest implementation here. + const xhr = new content.wrappedJSObject.XMLHttpRequest(); + xhr.open("GET", _url, true); + xhr.send(null); + }); + info("Wait for the request to be received by the netmonitor UI"); + return waitForRequests; +} diff --git a/devtools/client/netmonitor/test/browser_net_fonts.js b/devtools/client/netmonitor/test/browser_net_fonts.js new file mode 100644 index 0000000000..6667ca7962 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_fonts.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if font preview is generated correctly + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(FONTS_URL + "?name=fonts", { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Reload the page to get the font request + const waitForRequests = waitForNetworkEvents(monitor, 3); + await reloadBrowser(); + await waitForRequests; + + const wait = waitForDOMIfNeeded( + document, + "#response-panel .response-font[src^='data:']" + ); + + const requests = document.querySelectorAll( + ".request-list-item .requests-list-status" + ); + + // Check first font request + clickElement(requests[1], monitor); + clickOnSidebarTab(document, "response"); + + await wait; + + ok(true, "Font preview is shown"); + + const tabpanel = document.querySelector("#response-panel"); + let image = tabpanel.querySelector(".response-font"); + await once(image, "load"); + + ok( + image.complete && image.naturalHeight !== 0, + "Font preview got generated correctly" + ); + + let fontData = document.querySelectorAll(".tabpanel-summary-value"); + is(fontData[0].textContent, "Ostrich Sans Medium", "Font name is correct"); + // "font/ttf" is returned on Linux, which is the expected MIME type, though + // "application/octet-stream" is also accepted which is returned on Windows and MacOS. + ok( + ["font/ttf", "application/octet-stream"].includes(fontData[1].textContent), + "MIME type is correct" + ); + + // Check second font request + clickElement(requests[2], monitor); + + await waitForDOM(document, "#response-panel .response-font[src^='data:']"); + + image = tabpanel.querySelector(".response-font"); + await once(image, "load"); + + ok( + image.complete && image.naturalHeight !== 0, + "Font preview got generated correctly" + ); + + fontData = document.querySelectorAll(".tabpanel-summary-value"); + is(fontData[0].textContent, "Ostrich Sans Black", "Font name is correct"); + // Actually expected is "font/ttf", though "application/octet-stream" is + // ok as well and obviously returned when running the test locally. + ok( + ["font/ttf", "application/octet-stream"].includes(fontData[1].textContent), + "MIME type is correct" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_footer-summary.js b/devtools/client/netmonitor/test/browser_net_footer-summary.js new file mode 100644 index 0000000000..e0b12bcf77 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_footer-summary.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if the summary text displayed in the network requests menu footer is correct. + */ + +add_task(async function () { + const { + getFormattedSize, + getFormattedTime, + } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); + + requestLongerTimeout(2); + + const { tab, monitor } = await initNetMonitor(FILTERING_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequestsSummary } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const { L10N } = windowRequire("devtools/client/netmonitor/src/utils/l10n"); + const { PluralForm } = windowRequire("devtools/shared/plural-form"); + + store.dispatch(Actions.batchEnable(false)); + testStatus(); + + for (let i = 0; i < 2; i++) { + info(`Performing requests in batch #${i}`); + const wait = waitForNetworkEvents(monitor, 8); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.wrappedJSObject.performRequests( + '{ "getMedia": true, "getFlash": true }' + ); + }); + await wait; + + testStatus(); + + const buttons = ["html", "css", "js", "xhr", "fonts", "images", "media"]; + for (const button of buttons) { + const buttonEl = document.querySelector( + `.requests-list-filter-${button}-button` + ); + EventUtils.sendMouseEvent({ type: "click" }, buttonEl); + testStatus(); + } + } + + await teardown(monitor); + + function testStatus() { + const state = store.getState(); + const totalRequestsCount = state.requests.requests.length; + const requestsSummary = getDisplayedRequestsSummary(state); + info( + `Current requests: ${requestsSummary.count} of ${totalRequestsCount}.` + ); + + const valueCount = document.querySelector( + ".requests-list-network-summary-count" + ).textContent; + info("Current summary count: " + valueCount); + const expectedCount = PluralForm.get( + requestsSummary.count, + L10N.getStr("networkMenu.summary.requestsCount2") + ).replace("#1", requestsSummary.count); + + if (!totalRequestsCount || !requestsSummary.count) { + is( + valueCount, + L10N.getStr("networkMenu.summary.requestsCountEmpty"), + "The current summary text is incorrect, expected an 'empty' label." + ); + return; + } + + const valueTransfer = document.querySelector( + ".requests-list-network-summary-transfer" + ).textContent; + info("Current summary transfer: " + valueTransfer); + const expectedTransfer = L10N.getFormatStrWithNumbers( + "networkMenu.summary.transferred", + getFormattedSize(requestsSummary.contentSize), + getFormattedSize(requestsSummary.transferredSize) + ); + + const valueFinish = document.querySelector( + ".requests-list-network-summary-finish" + ).textContent; + info("Current summary finish: " + valueFinish); + const expectedFinish = L10N.getFormatStrWithNumbers( + "networkMenu.summary.finish", + getFormattedTime(requestsSummary.ms) + ); + + info(`Computed total bytes: ${requestsSummary.bytes}`); + info(`Computed total ms: ${requestsSummary.ms}`); + + is(valueCount, expectedCount, "The current summary count is correct."); + is( + valueTransfer, + expectedTransfer, + "The current summary transfer is correct." + ); + is(valueFinish, expectedFinish, "The current summary finish is correct."); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_frame.js b/devtools/client/netmonitor/test/browser_net_frame.js new file mode 100644 index 0000000000..f6731628b4 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_frame.js @@ -0,0 +1,273 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for all expected requests when an iframe is loading a subdocument. + */ + +const TOP_FILE_NAME = "html_frame-test-page.html"; +const SUB_FILE_NAME = "html_frame-subdocument.html"; +const TOP_URL = EXAMPLE_URL + TOP_FILE_NAME; +const SUB_URL = EXAMPLE_URL + SUB_FILE_NAME; + +const EXPECTED_REQUESTS_TOP = [ + { + method: "GET", + url: TOP_URL, + causeType: "document", + causeUri: null, + stack: true, + }, + { + method: "GET", + url: EXAMPLE_URL + "stylesheet_request", + causeType: "stylesheet", + causeUri: TOP_URL, + stack: false, + }, + { + method: "GET", + url: EXAMPLE_URL + "img_request", + causeType: "img", + causeUri: TOP_URL, + stack: false, + }, + { + method: "GET", + url: EXAMPLE_URL + "xhr_request", + causeType: "xhr", + causeUri: TOP_URL, + stack: [{ fn: "performXhrRequest", file: TOP_FILE_NAME, line: 25 }], + }, + { + method: "GET", + url: EXAMPLE_URL + "fetch_request", + causeType: "fetch", + causeUri: TOP_URL, + stack: [{ fn: "performFetchRequest", file: TOP_FILE_NAME, line: 29 }], + }, + { + method: "GET", + url: EXAMPLE_URL + "promise_fetch_request", + causeType: "fetch", + causeUri: TOP_URL, + stack: [ + { fn: "performPromiseFetchRequest", file: TOP_FILE_NAME, line: 41 }, + { + fn: null, + file: TOP_FILE_NAME, + line: 40, + asyncCause: "promise callback", + }, + ], + }, + { + method: "GET", + url: EXAMPLE_URL + "timeout_fetch_request", + causeType: "fetch", + causeUri: TOP_URL, + stack: [ + { fn: "performTimeoutFetchRequest", file: TOP_FILE_NAME, line: 43 }, + { + fn: "performPromiseFetchRequest", + file: TOP_FILE_NAME, + line: 42, + asyncCause: "setTimeout handler", + }, + ], + }, + { + method: "POST", + url: EXAMPLE_URL + "beacon_request", + causeType: "beacon", + causeUri: TOP_URL, + stack: [{ fn: "performBeaconRequest", file: TOP_FILE_NAME, line: 33 }], + }, +]; + +const EXPECTED_REQUESTS_SUB = [ + { + method: "GET", + url: SUB_URL, + causeType: "subdocument", + causeUri: TOP_URL, + stack: false, + }, + { + method: "GET", + url: EXAMPLE_URL + "stylesheet_request", + causeType: "stylesheet", + causeUri: SUB_URL, + stack: false, + }, + { + method: "GET", + url: EXAMPLE_URL + "img_request", + causeType: "img", + causeUri: SUB_URL, + stack: false, + }, + { + method: "GET", + url: EXAMPLE_URL + "xhr_request", + causeType: "xhr", + causeUri: SUB_URL, + stack: [{ fn: "performXhrRequest", file: SUB_FILE_NAME, line: 24 }], + }, + { + method: "GET", + url: EXAMPLE_URL + "fetch_request", + causeType: "fetch", + causeUri: SUB_URL, + stack: [{ fn: "performFetchRequest", file: SUB_FILE_NAME, line: 28 }], + }, + { + method: "GET", + url: EXAMPLE_URL + "promise_fetch_request", + causeType: "fetch", + causeUri: SUB_URL, + stack: [ + { fn: "performPromiseFetchRequest", file: SUB_FILE_NAME, line: 40 }, + { + fn: null, + file: SUB_FILE_NAME, + line: 39, + asyncCause: "promise callback", + }, + ], + }, + { + method: "GET", + url: EXAMPLE_URL + "timeout_fetch_request", + causeType: "fetch", + causeUri: SUB_URL, + stack: [ + { fn: "performTimeoutFetchRequest", file: SUB_FILE_NAME, line: 42 }, + { + fn: "performPromiseFetchRequest", + file: SUB_FILE_NAME, + line: 41, + asyncCause: "setTimeout handler", + }, + ], + }, + { + method: "POST", + url: EXAMPLE_URL + "beacon_request", + causeType: "beacon", + causeUri: SUB_URL, + stack: [{ fn: "performBeaconRequest", file: SUB_FILE_NAME, line: 32 }], + }, +]; + +const REQUEST_COUNT = + EXPECTED_REQUESTS_TOP.length + EXPECTED_REQUESTS_SUB.length; + +add_task(async function () { + // the initNetMonitor function clears the network request list after the + // page is loaded. That's why we first load a bogus page from SIMPLE_URL, + // and only then load the real thing from TOP_URL - we want to catch + // all the requests the page is making, not only the XHRs. + // We can't use about:blank here, because initNetMonitor checks that the + // page has actually made at least one request. + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + + const { document, store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + await navigateTo(TOP_URL); + + await waitForNetworkEvents(monitor, REQUEST_COUNT); + + is( + store.getState().requests.requests.length, + REQUEST_COUNT, + "All the page events should be recorded." + ); + + // Fetch stack-trace data from the backend and wait till + // all packets are received. + const requests = getSortedRequests(store.getState()); + await Promise.all( + requests.map(requestItem => + connector.requestData(requestItem.id, "stackTrace") + ) + ); + + // While there is a defined order for requests in each document separately, the requests + // from different documents may interleave in various ways that change per test run, so + // there is not a single order when considering all the requests together. + let currentTop = 0; + let currentSub = 0; + for (let i = 0; i < REQUEST_COUNT; i++) { + const requestItem = getSortedRequests(store.getState())[i]; + + const itemUrl = requestItem.url; + const itemCauseUri = requestItem.cause.loadingDocumentUri; + let spec; + if (itemUrl == SUB_URL || itemCauseUri == SUB_URL) { + spec = EXPECTED_REQUESTS_SUB[currentSub++]; + } else { + spec = EXPECTED_REQUESTS_TOP[currentTop++]; + } + const { method, url, causeType, causeUri, stack } = spec; + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + method, + url, + { cause: { type: causeType, loadingDocumentUri: causeUri } } + ); + + const { stacktrace } = requestItem; + const stackLen = stacktrace ? stacktrace.length : 0; + + if (stack) { + ok(stacktrace, `Request #${i} has a stacktrace`); + Assert.greater( + stackLen, + 0, + `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items` + ); + + // if "stack" is array, check the details about the top stack frames + if (Array.isArray(stack)) { + stack.forEach((frame, j) => { + is( + stacktrace[j].functionName, + frame.fn, + `Request #${i} has the correct function on JS stack frame #${j}` + ); + is( + stacktrace[j].filename.split("/").pop(), + frame.file, + `Request #${i} has the correct file on JS stack frame #${j}` + ); + is( + stacktrace[j].lineNumber, + frame.line, + `Request #${i} has the correct line number on JS stack frame #${j}` + ); + is( + stacktrace[j].asyncCause, + frame.asyncCause, + `Request #${i} has the correct async cause on JS stack frame #${j}` + ); + }); + } + } else { + is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`); + } + } + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_header-dns.js b/devtools/client/netmonitor/test/browser_net_header-dns.js new file mode 100644 index 0000000000..c7dc5b016a --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_header-dns.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if "DNS Resolution" is displayed in the headers panel and + * that requests that are resolve over DNS over HTTPS are accuratelly tracked. + * Note: Test has to run as a http3 test for the DoH servers to be used + */ + +add_task(async function testCheckDNSResolution() { + Services.dns.clearCache(true); + // Force to use DNS Trusted Recursive Resolver only + await pushPref("network.trr.mode", 3); + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + enableCache: true, + requestCount: 1, + }); + + const { document } = monitor.panelWin; + + const requestsComplete = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await requestsComplete; + + // Wait until the tab panel summary is displayed + const waitForTab = waitUntil( + () => document.querySelectorAll(".tabpanel-summary-label")[0] + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await waitForTab; + + const dnsEl = [...document.querySelectorAll(".headers-summary")].find( + el => + el.querySelector(".headers-summary-label").textContent === + "DNS Resolution" + ); + + is( + dnsEl.querySelector(".tabpanel-summary-value").textContent, + L10N.getStr(`netmonitor.headers.dns.overHttps`), + "The DNS Resolution value showed correctly" + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_header-docs.js b/devtools/client/netmonitor/test/browser_net_header-docs.js new file mode 100644 index 0000000000..91d2794d5f --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_header-docs.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if "Learn More" links are correctly displayed + * next to headers. + */ +add_task(async function () { + const { tab, monitor } = await initNetMonitor(POST_DATA_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const { + getHeadersURL, + } = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 2); + + AccessibilityUtils.setEnv({ + // Keyboard users will will see the sidebar when the request row is + // selected. Accessibility is handled on the container level. + actionCountRule: false, + interactiveRule: false, + labelRule: false, + }); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelectorAll(".request-list-item")[0] + ); + AccessibilityUtils.resetEnv(); + + testShowLearnMore(getSortedRequests(store.getState())[0]); + + return teardown(monitor); + + /* + * Tests that a "Learn More" button is only shown if + * and only if a header is documented in MDN. + */ + function testShowLearnMore(data) { + const selector = ".properties-view .treeRow.stringRow"; + document.querySelectorAll(selector).forEach((rowEl, index) => { + const headerName = rowEl.querySelectorAll(".treeLabelCell .treeLabel")[0] + .textContent; + const headerDocURL = getHeadersURL(headerName); + const learnMoreEl = rowEl.querySelectorAll( + ".treeValueCell .learn-more-link" + ); + + if (headerDocURL === null) { + Assert.strictEqual( + learnMoreEl.length, + 0, + 'undocumented header does not include a "Learn More" button' + ); + } else { + Assert.strictEqual( + learnMoreEl[0].getAttribute("title"), + headerDocURL, + 'documented header includes a "Learn More" button with a link to MDN' + ); + } + }); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_header-ref-policy.js b/devtools/client/netmonitor/test/browser_net_header-ref-policy.js new file mode 100644 index 0000000000..18e9578bb5 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_header-ref-policy.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if "Referrer Policy" is displayed in the header panel. + */ +add_task(async function () { + const { tab, monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Execute request. + await performRequests(monitor, tab, 1); + + // Wait until the tab panel summary is displayed + const wait = waitUntil( + () => document.querySelectorAll(".tabpanel-summary-label")[0] + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + const referrerPolicyIndex = 3; + const referrerPolicyHeader = document.querySelectorAll( + ".tabpanel-summary-label" + )[referrerPolicyIndex]; + const referrerPolicyValue = document.querySelectorAll( + ".tabpanel-summary-value" + )[referrerPolicyIndex]; + + is( + referrerPolicyHeader.textContent === "Referrer Policy", + true, + '"Referrer Policy" header is displayed in the header panel.' + ); + + const defaultPolicy = Services.prefs.getIntPref( + "network.http.referer.defaultPolicy" + ); + const stringMap = { + 0: "no-referrer", + 1: "same-origin", + 2: "strict-origin-when-cross-origin", + 3: "no-referrer-when-downgrade", + }; + is( + referrerPolicyValue.textContent === stringMap[defaultPolicy], + true, + "The referrer policy value is reflected correctly." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_header-request-priority.js b/devtools/client/netmonitor/test/browser_net_header-request-priority.js new file mode 100644 index 0000000000..196116a98d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_header-request-priority.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if "Request Priority" is displayed in the header panel. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + + const { document } = monitor.panelWin; + + const waitReq = waitForNetworkEvents(monitor, 1); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-reload-notice-button") + ); + await waitReq; + + // Wait until the tab panel summary is displayed + const wait = waitUntil( + () => document.querySelectorAll(".tabpanel-summary-label")[0] + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + const requestPriorityHeaderExists = Array.from( + document.querySelectorAll(".tabpanel-summary-label") + ).some(header => header.textContent === "Request Priority"); + is( + requestPriorityHeaderExists, + true, + '"Request Priority" header is displayed in the header panel.' + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_headers-alignment.js b/devtools/client/netmonitor/test/browser_net_headers-alignment.js new file mode 100644 index 0000000000..b6eaca9308 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_headers-alignment.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1360457 - Mis-alignment between headers and columns on overflow + */ + +add_task(async function () { + requestLongerTimeout(4); + + const { tab, monitor } = await initNetMonitor(INFINITE_GET_URL, { + enableCache: true, + requestCount: 1, + }); + const { document, windowRequire, store } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait until the first request makes the empty notice disappear + await waitForRequestListToAppear(); + + const requestsContainerScroll = document.querySelector( + ".requests-list-scroll" + ); + ok(requestsContainerScroll, "Container element exists as expected."); + const requestsContainer = document.querySelector(".requests-list-row-group"); + const headers = document.querySelector(".requests-list-headers"); + ok(headers, "Headers element exists as expected."); + + await waitForRequestsToOverflowContainer(monitor, requestsContainerScroll); + + testColumnsAlignment(headers, requestsContainer); + + // Stop doing requests. + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.wrappedJSObject.stopRequests(); + }); + + // Done: clean up. + return teardown(monitor); + + function waitForRequestListToAppear() { + info( + "Waiting until the empty notice disappears and is replaced with the list" + ); + return waitUntil( + () => !!document.querySelector(".requests-list-row-group") + ); + } +}); + +async function waitForRequestsToOverflowContainer(monitor, requestList) { + info("Waiting for enough requests to overflow the container"); + while (true) { + info("Waiting for one network request"); + await waitForNetworkEvents(monitor, 1); + if (requestList.scrollHeight > requestList.clientHeight + 50) { + info("The list is long enough, returning"); + return; + } + } +} diff --git a/devtools/client/netmonitor/test/browser_net_headers-link_clickable.js b/devtools/client/netmonitor/test/browser_net_headers-link_clickable.js new file mode 100644 index 0000000000..9f59e7fe12 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_headers-link_clickable.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if links in headers panel are clickable. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(JSON_LONG_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + await performRequests(monitor, tab, 1); + + info("Selecting first request"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + + info("Waiting for request and response headers"); + await waitForRequestData(store, ["requestHeaders", "responseHeaders"]); + + const headerLink = document.querySelector(".objectBox-string .url"); + const expectedURL = + "http://example.com/browser/devtools/client/netmonitor/test/html_json-long-test-page.html"; + const onTabOpen = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL, true); + + info("Click on a first link in Headers panel"); + headerLink.click(); + await onTabOpen; + + ok(onTabOpen, "New tab opened from link"); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_headers-proxy.js b/devtools/client/netmonitor/test/browser_net_headers-proxy.js new file mode 100644 index 0000000000..a855b98b74 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_headers-proxy.js @@ -0,0 +1,211 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the proxy information is displayed in the netmonitor +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document } = monitor.panelWin; + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + // Wait until the tab panel summary is displayed + const waitForTab = waitUntil(() => + document.querySelector(".tabpanel-summary-label") + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelector(".request-list-item") + ); + await waitForTab; + + // Expand preview + await toggleUrlPreview(true, monitor); + + const proxyAddressEl = [...document.querySelectorAll(".treeRow")].find( + el => el.querySelector(".treeLabelCell")?.textContent === "Proxy Address" + ); + + is( + proxyAddressEl.querySelector(".treeValueCell").innerText, + "127.0.0.1:4443", + "The remote proxy address summary value is correct." + ); + + is( + document.querySelector(".headers-proxy-status .headers-summary-label") + .textContent, + "Proxy Status", + "The proxy status header is displayed" + ); + + is( + document.querySelector(".headers-proxy-status .tabpanel-summary-value") + .textContent, + "200Connected", + "The proxy status value showed correctly" + ); + + is( + document.querySelector(".headers-proxy-version .tabpanel-summary-label") + .textContent, + "Proxy Version", + "The proxy http version header is displayed" + ); + + is( + document.querySelector(".headers-proxy-version .tabpanel-summary-value") + .textContent, + "HTTP/1.1", + "The proxy http version value showed correctly" + ); + + await teardown(monitor); +}); + +const noProxyServerUrl = createTestHTTPServer(); +noProxyServerUrl.registerPathHandler("/index.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", true); + response.write("<html> SIMPLE DOCUMENT </html>"); +}); + +const NO_PROXY_SERVER_URL = `http://localhost:${noProxyServerUrl.identity.primaryPort}`; + +// Test that the proxy information is not displayed in the netmonitor +add_task(async function () { + const { monitor } = await initNetMonitor(NO_PROXY_SERVER_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document } = monitor.panelWin; + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + // Wait until the tab panel summary is displayed + const waitForTab = waitUntil(() => + document.querySelector(".tabpanel-summary-label") + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelector(".request-list-item") + ); + await waitForTab; + + // Expand preview + await toggleUrlPreview(true, monitor); + + const addressEl = [...document.querySelectorAll(".treeRow")].find( + el => el.querySelector(".treeLabelCell")?.textContent === "Address" + ); + + ok(addressEl, "The address is not the proxy address"); + + ok( + !document.querySelector(".headers-proxy-status"), + "The proxy status header is not displayed" + ); + + ok( + !document.querySelector(".headers-proxy-version"), + "The proxy http version header is not displayed" + ); + + await teardown(monitor); +}); + +const serverBehindFakeProxy = createTestHTTPServer(); +const fakeProxy = createTestHTTPServer(); + +fakeProxy.identity.add( + "http", + "localhost", + serverBehindFakeProxy.identity.primaryPort +); +fakeProxy.registerPrefixHandler("/", (request, response) => { + if (request.hasHeader("Proxy-Authorization")) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", true); + response.write("ok, got proxy auth"); + } else { + response.setStatusLine( + request.httpVersion, + 407, + "Proxy authentication required" + ); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Proxy-Authenticate", 'Basic realm="foobar"', false); + response.write("auth required"); + } +}); + +const SERVER_URL = `http://localhost:${serverBehindFakeProxy.identity.primaryPort}`; + +// Test that `Proxy-Authorization` request header is not shown for in the headers panel +add_task(async function () { + await pushPref("network.proxy.type", 1); + await pushPref("network.proxy.http", "localhost"); + await pushPref("network.proxy.http_port", fakeProxy.identity.primaryPort); + await pushPref("network.proxy.allow_hijacking_localhost", true); + + // Wait for initial primary password dialog after opening the tab. + const onDialog = TestUtils.topicObserved("common-dialog-loaded"); + + const tab = await addTab(SERVER_URL, { waitForLoad: false }); + + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "netmonitor", + }); + info("Network monitor pane shown successfully."); + + const monitor = toolbox.getCurrentPanel(); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const [subject] = await onDialog; + const dialog = subject.Dialog; + + ok(true, `Authentication dialog displayed`); + + info("Fill in login and password, and validate dialog"); + dialog.ui.loginTextbox.value = "user"; + dialog.ui.password1Textbox.value = "pass"; + + const onDialogClosed = BrowserTestUtils.waitForEvent( + window, + "DOMModalDialogClosed" + ); + dialog.ui.button0.click(); + await onDialogClosed; + ok(true, "Dialog is closed"); + + const requestEl = await waitFor(() => + document.querySelector(".request-list-item") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, requestEl); + + await waitUntil(() => document.querySelector(".headers-overview")); + + const headersPanel = document.querySelector("#headers-panel"); + const headerIsFound = [ + ...headersPanel.querySelectorAll("tr .treeLabelCell .treeLabel"), + ].some(headerEl => headerEl.innerText == "Proxy-Authorization"); + + ok( + !headerIsFound, + "The `Proxy-Authorization` should not be displayed in the Headers panel" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_headers-resize.js b/devtools/client/netmonitor/test/browser_net_headers-resize.js new file mode 100644 index 0000000000..dc1aadb470 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_headers-resize.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests resizing of columns in NetMonitor. + */ +add_task(async function () { + await testForGivenDir("ltr"); + + await testForGivenDir("rtl"); +}); + +async function testForGivenDir(dir) { + if (dir === "rtl") { + await pushPref("intl.l10n.pseudo", "bidi"); + } else { + await pushPref("intl.l10n.pseudo", ""); + } + + // Reset visibleColumns so we only get the default ones + // and not all that are set in head.js + Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns"); + const initialColumnData = Services.prefs.getCharPref( + "devtools.netmonitor.columnsData" + ); + let visibleColumns = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.visibleColumns") + ); + + // Init network monitor + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, windowRequire, store } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Wait for network events (to have some requests in the table) + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + const headers = document.querySelector(".requests-list-headers"); + const parentWidth = headers.getBoundingClientRect().width; + + // 1. Change File column from 25% (default) to 20% + // Size column should then change from 5% (default) to 10% + // When File width changes, contentSize should compensate the change. + info("Resize file & check changed prefs..."); + const fileHeader = document.querySelector(`#requests-list-file-header-box`); + + resizeColumn(fileHeader, 20, parentWidth, dir); + + // after resize - get fresh prefs for tests + let columnsData = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.columnsData") + ); + checkColumnsData(columnsData, "file", 20); + checkColumnsData(columnsData, "contentSize", 10); + checkSumOfVisibleColumns(columnsData, visibleColumns); + + // 2. Change Waterfall column width and check that the size + // of waterfall changed correctly and all the other columns changed size. + info("Resize waterfall & check changed prefs..."); + const waterfallHeader = document.querySelector( + `#requests-list-waterfall-header-box` + ); + // before resizing waterfall -> save old columnsData for later testing + const oldColumnsData = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.columnsData") + ); + resizeWaterfallColumn(waterfallHeader, 30, parentWidth, dir); // 30 fails currently! + + // after resize - get fresh prefs for tests + columnsData = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.columnsData") + ); + + checkColumnsData(columnsData, "waterfall", 30); + checkSumOfVisibleColumns(columnsData, visibleColumns); + checkAllColumnsChanged(columnsData, oldColumnsData, visibleColumns); + + // 3. Check that all rows have the right column sizes. + info("Checking alignment of columns and headers..."); + const requestsContainer = document.querySelector(".requests-list-row-group"); + testColumnsAlignment(headers, requestsContainer); + + // 4. Hide all columns but size and waterfall + // and check that they resize correctly. Then resize + // waterfall to 50% => size should take up 50% + info("Hide all but 2 columns - size & waterfall and check resizing..."); + await hideMoreColumns(monitor, [ + "status", + "method", + "domain", + "file", + "initiator", + "type", + "transferred", + ]); + + resizeWaterfallColumn(waterfallHeader, 50, parentWidth, dir); + // after resize - get fresh prefs for tests + columnsData = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.columnsData") + ); + visibleColumns = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.visibleColumns") + ); + + checkColumnsData(columnsData, "contentSize", 50); + checkColumnsData(columnsData, "waterfall", 50); + checkSumOfVisibleColumns(columnsData, visibleColumns); + + // 5. Hide all columns but domain and file + // and resize domain to 50% => file should be 50% + info("Hide all but 2 columns - domain & file and check resizing..."); + await showMoreColumns(monitor, ["domain", "file"]); + await hideMoreColumns(monitor, ["contentSize", "waterfall"]); + + const domainHeader = document.querySelector( + `#requests-list-domain-header-box` + ); + resizeColumn(domainHeader, 50, parentWidth, dir); + + // after resize - get fresh prefs for tests + columnsData = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.columnsData") + ); + + visibleColumns = JSON.parse( + Services.prefs.getCharPref("devtools.netmonitor.visibleColumns") + ); + + checkColumnsData(columnsData, "domain", 50); + checkColumnsData(columnsData, "file", 50); + checkSumOfVisibleColumns(columnsData, visibleColumns); + + // Done: clean up. + Services.prefs.setCharPref( + "devtools.netmonitor.columnsData", + initialColumnData + ); + return teardown(monitor); +} + +async function hideMoreColumns(monitor, arr) { + for (let i = 0; i < arr.length; i++) { + await hideColumn(monitor, arr[i]); + } +} + +async function showMoreColumns(monitor, arr) { + for (let i = 0; i < arr.length; i++) { + await showColumn(monitor, arr[i]); + } +} + +function resizeColumn(columnHeader, newPercent, parentWidth, dir) { + const newWidthInPixels = (newPercent * parentWidth) / 100; + const win = columnHeader.ownerDocument.defaultView; + const currentWidth = columnHeader.getBoundingClientRect().width; + const mouseDown = dir === "rtl" ? 0 : currentWidth; + const mouseMove = + dir === "rtl" ? currentWidth - newWidthInPixels : newWidthInPixels; + + EventUtils.synthesizeMouse( + columnHeader, + mouseDown, + 1, + { type: "mousedown" }, + win + ); + EventUtils.synthesizeMouse( + columnHeader, + mouseMove, + 1, + { type: "mousemove" }, + win + ); + EventUtils.synthesizeMouse( + columnHeader, + mouseMove, + 1, + { type: "mouseup" }, + win + ); +} + +function resizeWaterfallColumn(columnHeader, newPercent, parentWidth, dir) { + const newWidthInPixels = (newPercent * parentWidth) / 100; + const win = columnHeader.ownerDocument.defaultView; + const mouseDown = + dir === "rtl" + ? columnHeader.getBoundingClientRect().right + : columnHeader.getBoundingClientRect().left; + const mouseMove = + dir === "rtl" + ? mouseDown + + (newWidthInPixels - columnHeader.getBoundingClientRect().width) + : mouseDown + + (columnHeader.getBoundingClientRect().width - newWidthInPixels); + + EventUtils.synthesizeMouse( + columnHeader.parentElement, + mouseDown, + 1, + { type: "mousedown" }, + win + ); + EventUtils.synthesizeMouse( + columnHeader.parentElement, + mouseMove, + 1, + { type: "mousemove" }, + win + ); + EventUtils.synthesizeMouse( + columnHeader.parentElement, + mouseMove, + 1, + { type: "mouseup" }, + win + ); +} + +function checkColumnsData(columnsData, column, expectedWidth) { + const widthInPref = Math.round(getWidthFromPref(columnsData, column)); + is(widthInPref, expectedWidth, "Column " + column + " has expected size."); +} + +function checkSumOfVisibleColumns(columnsData, visibleColumns) { + let sum = 0; + visibleColumns.forEach(column => { + sum += getWidthFromPref(columnsData, column); + }); + sum = Math.round(sum); + is(sum, 100, "All visible columns cover 100%."); +} + +function getWidthFromPref(columnsData, column) { + const widthInPref = columnsData.find(function (element) { + return element.name === column; + }).width; + return widthInPref; +} + +function checkAllColumnsChanged(columnsData, oldColumnsData, visibleColumns) { + const oldWaterfallWidth = getWidthFromPref(oldColumnsData, "waterfall"); + const newWaterfallWidth = getWidthFromPref(columnsData, "waterfall"); + visibleColumns.forEach(column => { + // do not test waterfall against waterfall + if (column !== "waterfall") { + const oldWidth = getWidthFromPref(oldColumnsData, column); + const newWidth = getWidthFromPref(columnsData, column); + + // Test that if waterfall is smaller all other columns are bigger + if (oldWaterfallWidth > newWaterfallWidth) { + is( + oldWidth < newWidth, + true, + "Column " + column + " has changed width correctly." + ); + } + // Test that if waterfall is bigger all other columns are smaller + if (oldWaterfallWidth < newWaterfallWidth) { + is( + oldWidth > newWidth, + true, + "Column " + column + " has changed width correctly." + ); + } + } + }); +} diff --git a/devtools/client/netmonitor/test/browser_net_headers_filter.js b/devtools/client/netmonitor/test/browser_net_headers_filter.js new file mode 100644 index 0000000000..6256adb08d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_headers_filter.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if Request-Headers and Response-Headers are correctly filtered in Headers tab. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_SJS, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + wait = waitUntil(() => document.querySelector(".headers-overview")); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + await waitForRequestData(store, ["requestHeaders", "responseHeaders"]); + + document.querySelectorAll(".devtools-filterinput")[1].focus(); + EventUtils.synthesizeKey("con", {}); + await waitUntil(() => document.querySelector(".treeRow.hidden")); + + info("Check if Headers are filtered correctly"); + + const expectedResponseHeaders = [ + "cache-control", + "connection", + "content-length", + "content-type", + ]; + const expectedRequestHeaders = ["Cache-Control", "Connection"]; + + const responseLabelCells = document.querySelectorAll( + "#responseHeaders .treeLabelCell" + ); + const requestLabelCells = document.querySelectorAll( + "#requestHeaders .treeLabelCell" + ); + const filteredResponseHeaders = []; + const filteredRequestHeaders = []; + + for (let i = 0; i < responseLabelCells.length; i++) { + if (responseLabelCells[i].offsetHeight > 0) { + filteredResponseHeaders.push(responseLabelCells[i].innerText); + } + } + + for (let i = 0; i < requestLabelCells.length; i++) { + if (requestLabelCells[i].offsetHeight > 0) { + filteredRequestHeaders.push(requestLabelCells[i].innerText); + } + } + + is( + filteredResponseHeaders.toString(), + expectedResponseHeaders.toString(), + "Response Headers are filtered" + ); + is( + filteredRequestHeaders.toString(), + expectedRequestHeaders.toString(), + "Request Headers are filtered" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_headers_sorted.js b/devtools/client/netmonitor/test/browser_net_headers_sorted.js new file mode 100644 index 0000000000..2965ea8d3c --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_headers_sorted.js @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if Request-Headers and Response-Headers are sorted in Headers tab. + * The test also verifies that headers with the same name and headers + * with an empty value are also displayed. + * + * The test also checks that raw headers are displayed in the original + * order and not sorted. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_SJS, { + requestCount: 1, + }); + info("Starting test... "); + + const { store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + // Verify request and response headers. + await verifyHeaders(monitor); + await verifyRawHeaders(monitor); + + // Clean up + await teardown(monitor); +}); + +async function verifyHeaders(monitor) { + const { document, store } = monitor.panelWin; + + info("Check if Request-Headers and Response-Headers are sorted"); + + const wait = waitForDOM(document, ".headers-overview"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + await waitForRequestData(store, ["requestHeaders", "responseHeaders"]); + + const expectedResponseHeaders = [ + "cache-control", + "connection", + "content-length", + "content-type", + "date", + "expires", + "foo-bar", + "foo-bar", + "foo-bar", + "pragma", + "server", + "set-cookie", + "set-cookie", + ]; + const expectedRequestHeaders = [ + "Accept", + "Accept-Encoding", + "Accept-Language", + "Cache-Control", + "Connection", + "Cookie", + "Host", + "Pragma", + "Sec-Fetch-Dest", + "Sec-Fetch-Mode", + "Sec-Fetch-Site", + "Upgrade-Insecure-Requests", + "User-Agent", + ]; + + const responseLabelCells = document.querySelectorAll( + "#responseHeaders .treeLabelCell" + ); + const requestLabelCells = document.querySelectorAll( + "#requestHeaders .treeLabelCell" + ); + const actualResponseHeaders = []; + const actualRequestHeaders = []; + + for (let i = 0; i < responseLabelCells.length; i++) { + actualResponseHeaders.push(responseLabelCells[i].innerText); + } + + for (let i = 0; i < requestLabelCells.length; i++) { + actualRequestHeaders.push(requestLabelCells[i].innerText); + } + + is( + actualResponseHeaders.toString(), + expectedResponseHeaders.toString(), + "Response Headers are sorted" + ); + + is( + actualRequestHeaders.toString(), + expectedRequestHeaders.toString(), + "Request Headers are sorted" + ); +} + +async function verifyRawHeaders(monitor) { + const { document } = monitor.panelWin; + + info("Check if raw Request-Headers and raw Response-Headers are not sorted"); + + const actualResponseHeaders = []; + const actualRequestHeaders = []; + + const expectedResponseHeaders = [ + "cache-control", + "pragma", + "expires", + "set-cookie", + "set-cookie", + "content-type", + "foo-bar", + "foo-bar", + "foo-bar", + "connection", + "server", + "date", + "content-length", + ]; + + const expectedRequestHeaders = [ + "Host", + "User-Agent", + "Accept", + "Accept-Language", + "Accept-Encoding", + "Connection", + "Cookie", + "Upgrade-Insecure-Requests", + "Sec-Fetch-Dest", + "Sec-Fetch-Mode", + "Sec-Fetch-Site", + "Pragma", + "Cache-Control", + ]; + + // Click the 'Raw headers' toggle to show original headers source. + for (const rawToggleInput of document.querySelectorAll( + ".devtools-checkbox-toggle" + )) { + rawToggleInput.click(); + } + + // Wait till raw headers are available. + let rawArr; + await waitUntil(() => { + rawArr = document.querySelectorAll("textarea.raw-headers"); + // Both raw headers must be present + return rawArr.length > 1; + }); + + // Request headers are rendered first, so it is element with index 1 + const requestHeadersText = rawArr[1].textContent; + // Response headers are rendered first, so it is element with index 0 + const responseHeadersText = rawArr[0].textContent; + + const rawRequestHeadersArray = requestHeadersText.split("\n"); + for (let i = 1; i < rawRequestHeadersArray.length; i++) { + const header = rawRequestHeadersArray[i]; + actualRequestHeaders.push(header.split(":")[0]); + } + + const rawResponseHeadersArray = responseHeadersText.split("\n"); + for (let i = 1; i < rawResponseHeadersArray.length; i++) { + const header = rawResponseHeadersArray[i]; + actualResponseHeaders.push(header.split(":")[0]); + } + + is( + actualResponseHeaders.toString(), + expectedResponseHeaders.toString(), + "Raw Response Headers are not sorted" + ); + + is( + actualRequestHeaders.toString(), + expectedRequestHeaders.toString(), + "Raw Request Headers are not sorted" + ); +} diff --git a/devtools/client/netmonitor/test/browser_net_html-preview.js b/devtools/client/netmonitor/test/browser_net_html-preview.js new file mode 100644 index 0000000000..873c712105 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_html-preview.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if different response content types are handled correctly. + */ + +const httpServer = createTestHTTPServer(); +httpServer.registerContentType("html", "text/html"); + +const BASE_URL = `http://localhost:${httpServer.identity.primaryPort}/`; + +const REDIRECT_URL = BASE_URL + "redirect.html"; + +// In all content previewed as HTML we ensure using proper html, head and body in order to +// prevent having them added by the <browser> when loaded as a preview. +function addBaseHtmlElements(body) { + return `<html><head></head><body>${body}</body></html>`; +} + +// This first page asserts we can redirect to another URL, even if JS happen to be executed +const FETCH_CONTENT_1 = addBaseHtmlElements( + `Fetch 1<script>window.parent.location.href = "${REDIRECT_URL}";</script>` +); +// This second page asserts that JS is disabled +const FETCH_CONTENT_2 = addBaseHtmlElements( + `Fetch 2<script>document.write("JS activated")</script>` +); +// This third page asserts that links and forms are disabled +const FETCH_CONTENT_3 = addBaseHtmlElements( + `Fetch 3<a href="${REDIRECT_URL}">link</a> -- <form action="${REDIRECT_URL}"><input type="submit"></form>` +); +// This fourth page asserts responses with line breaks +const FETCH_CONTENT_4 = addBaseHtmlElements(` + <a href="#" id="link1">link1</a> + <a href="#" id="link2">link2</a> +`); + +// Use fetch in order to prevent actually running this code in the test page +const TEST_HTML = addBaseHtmlElements(`<div id="to-copy">HTML</div><script> + fetch("${BASE_URL}fetch-1.html"); + fetch("${BASE_URL}fetch-2.html"); + fetch("${BASE_URL}fetch-3.html"); + fetch("${BASE_URL}fetch-4.html"); +</script>`); +const TEST_URL = BASE_URL + "doc-html-preview.html"; + +httpServer.registerPathHandler( + "/doc-html-preview.html", + (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(TEST_HTML); + } +); +httpServer.registerPathHandler("/fetch-1.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(FETCH_CONTENT_1); +}); +httpServer.registerPathHandler("/fetch-2.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(FETCH_CONTENT_2); +}); +httpServer.registerPathHandler("/fetch-3.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(FETCH_CONTENT_3); +}); +httpServer.registerPathHandler("/fetch-4.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(FETCH_CONTENT_4); +}); +httpServer.registerPathHandler("/redirect.html", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write("Redirected!"); +}); + +add_task(async function () { + // Enable async events so that clicks on preview iframe's links are correctly + // going through the parent process which is meant to cancel any mousedown. + await pushPref("test.events.async.enabled", true); + + const { monitor } = await initNetMonitor(TEST_URL, { requestCount: 3 }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const onNetworkEvent = waitForNetworkEvents(monitor, 3); + await reloadBrowser(); + await onNetworkEvent; + + // The new lines are stripped when using outerHTML to retrieve HTML content of the preview iframe + await selectIndexAndWaitForHtmlView(0, TEST_HTML); + await selectIndexAndWaitForHtmlView(1, FETCH_CONTENT_1); + await selectIndexAndWaitForHtmlView(2, FETCH_CONTENT_2); + await selectIndexAndWaitForHtmlView(3, FETCH_CONTENT_3); + await selectIndexAndWaitForHtmlView(4, FETCH_CONTENT_4); + + await teardown(monitor); + + async function selectIndexAndWaitForHtmlView(index, expectedHtmlPreview) { + info(`Select the request #${index}`); + const onResponseContent = monitor.panelWin.api.once( + TEST_EVENTS.RECEIVED_RESPONSE_CONTENT + ); + store.dispatch(Actions.selectRequestByIndex(index)); + + info("Open the Response tab"); + document.querySelector("#response-tab").click(); + + const [iframe] = await waitForDOM( + document, + "#response-panel .html-preview iframe" + ); + + // <xul:iframe type=content remote=true> don't emit "load" event. + // And SpecialPowsers.spawn throws if kept running during a page load. + // So poll for the end of the iframe load... + await waitFor(async () => { + // Note that if spawn executes early, the iframe may not yet be loading + // and would throw for the reason mentioned in previous comment. + try { + const rv = await SpecialPowers.spawn(iframe.browsingContext, [], () => { + return content.document.readyState == "complete"; + }); + return rv; + } catch (e) { + return false; + } + }); + + info("Wait for response content to be loaded"); + await onResponseContent; + + is( + iframe.browsingContext.currentWindowGlobal.isInProcess, + false, + "The preview is loaded in a content process" + ); + + await SpecialPowers.spawn( + iframe.browsingContext, + [expectedHtmlPreview], + async function (expectedHtml) { + is( + content.document.documentElement.outerHTML, + expectedHtml, + "The text shown in the iframe is incorrect for the html request." + ); + } + ); + + // Only assert copy to clipboard on the first test page + if (expectedHtmlPreview == TEST_HTML) { + await waitForClipboardPromise(async function () { + await SpecialPowers.spawn( + iframe.browsingContext, + [], + async function () { + const elt = content.document.getElementById("to-copy"); + EventUtils.synthesizeMouseAtCenter(elt, { clickCount: 2 }, content); + await new Promise(r => + elt.addEventListener("dblclick", r, { once: true }) + ); + EventUtils.synthesizeKey("c", { accelKey: true }, content); + } + ); + }, "HTML"); + } + + return iframe; + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_http3_request_details.js b/devtools/client/netmonitor/test/browser_net_http3_request_details.js new file mode 100644 index 0000000000..9ceb9dba88 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_http3_request_details.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests request details with HTTP/3 + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_SJS, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + const waitForHeaders = waitForDOM(document, ".headers-overview"); + store.dispatch(Actions.toggleNetworkDetails()); + await waitForHeaders; + + info("Assert the content of the headers"); + + const tabpanel = document.querySelector("#headers-panel"); + // Request URL + is( + tabpanel.querySelector(".url-preview .url").innerText, + HTTPS_SIMPLE_SJS, + "The url summary value is incorrect." + ); + + // Request method + is( + tabpanel.querySelectorAll(".treeLabel")[0].innerText, + "GET", + "The method summary value is incorrect." + ); + // Status code + is( + tabpanel.querySelector(".requests-list-status-code").innerText, + "200", + "The status summary code is incorrect." + ); + is( + tabpanel.querySelector(".status").childNodes[1].textContent, + "", // HTTP/2 and 3 send no status text, only a code + "The status summary value is incorrect." + ); + // Version + is( + tabpanel.querySelectorAll(".tabpanel-summary-value")[1].innerText, + "HTTP/3", + "The HTTP version is incorrect." + ); + + await waitForRequestData(store, ["requestHeaders", "responseHeaders"]); + + is( + tabpanel.querySelectorAll(".accordion-item").length, + 2, + "There should be 2 header scopes displayed in this tabpanel." + ); + + const headers = [...tabpanel.querySelectorAll(".accordion .treeLabelCell")]; + + is( + // The Text-Encoding header is not consistently displayed, exclude it from + // the assertion. See Bug 1830053. + headers.filter(cell => cell.textContent != "TE").length, + 25, + "There should be 25 header values displayed in this tabpanel." + ); + + const headersTable = tabpanel.querySelector(".accordion"); + const responseHeaders = headersTable.querySelectorAll( + "tr[id^='/Response Headers']" + ); + + const expectedHeaders = [ + { + name: "cache-control", + value: "no-cache, no-store, must-revalidate", + index: 0, + }, + { + name: "content-length", + value: "12", + index: 1, + }, + { + name: "content-type", + value: "text/plain; charset=utf-8", + index: 2, + }, + { + name: "foo-bar", + value: "baz", + index: 6, + }, + ]; + expectedHeaders.forEach(header => { + is( + responseHeaders[header.index].querySelector(".treeLabel").innerHTML, + header.name, + `The response header at index ${header.index} name was incorrect.` + ); + is( + responseHeaders[header.index].querySelector(".objectBox").innerHTML, + `${header.value}`, + `The response header at index ${header.index} value was incorrect.` + ); + }); + + info("Assert the content of the raw headers"); + + // Click the 'Raw headers' toggle to show original headers source. + document.querySelector("#raw-request-checkbox").click(); + document.querySelector("#raw-response-checkbox").click(); + + let rawHeadersElements; + await waitUntil(() => { + rawHeadersElements = document.querySelectorAll("textarea.raw-headers"); + // Both raw headers must be present + return rawHeadersElements.length > 1; + }); + const requestHeadersText = rawHeadersElements[1].textContent; + const rawRequestHeaderFirstLine = requestHeadersText.split(/\r\n|\n|\r/)[0]; + is( + rawRequestHeaderFirstLine, + "GET /browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs HTTP/3" + ); + + const responseHeadersText = rawHeadersElements[0].textContent; + const rawResponseHeaderFirstLine = responseHeadersText.split(/\r\n|\n|\r/)[0]; + is(rawResponseHeaderFirstLine, "HTTP/3 200 "); // H2/3 send no status text + + info("Assert the content of the protocol column"); + const target = document.querySelectorAll(".request-list-item")[0]; + is( + target.querySelector(".requests-list-protocol").textContent, + "HTTP/3", + "The displayed protocol is correct." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_image-tooltip.js b/devtools/client/netmonitor/test/browser_net_image-tooltip.js new file mode 100644 index 0000000000..c0b08708e8 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_image-tooltip.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const IMAGE_TOOLTIP_URL = EXAMPLE_URL + "html_image-tooltip-test-page.html"; +const IMAGE_TOOLTIP_REQUESTS = 1; + +/** + * Tests if image responses show a popup in the requests menu when hovered. + */ +add_task(async function test() { + const { tab, monitor } = await initNetMonitor(IMAGE_TOOLTIP_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { triggerActivity } = connector; + const { ACTIVITY_TYPE } = windowRequire( + "devtools/client/netmonitor/src/constants" + ); + const toolboxDoc = monitor.panelWin.parent.document; + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, IMAGE_TOOLTIP_REQUESTS); + + info("Checking the image thumbnail after a few requests were made..."); + await showTooltipAndVerify( + document.querySelectorAll(".request-list-item")[0] + ); + + // Hide tooltip before next test, to avoid the situation that tooltip covers + // the icon for the request of the next test. + info("Checking the image thumbnail gets hidden..."); + await hideTooltipAndVerify( + document.querySelectorAll(".request-list-item")[0] + ); + + // +1 extra document reload + const onEvents = waitForNetworkEvents(monitor, IMAGE_TOOLTIP_REQUESTS + 1); + + info("Reloading the debuggee and performing all requests again..."); + await triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.wrappedJSObject.performRequests(); + }); + await onEvents; + + info("Checking the image thumbnail after a reload."); + await showTooltipAndVerify( + document.querySelectorAll(".request-list-item")[1] + ); + + info( + "Checking if the image thumbnail is hidden when mouse leaves the menu widget" + ); + const requestsListContents = document.querySelector( + ".requests-list-row-group" + ); + EventUtils.synthesizeMouse( + requestsListContents, + 0, + 0, + { type: "mousemove" }, + monitor.panelWin + ); + await waitUntil( + () => !toolboxDoc.querySelector(".tooltip-container.tooltip-visible") + ); + + await teardown(monitor); + + /** + * Show a tooltip on the {target} and verify that it was displayed + * with the expected content. + */ + async function showTooltipAndVerify(target) { + const anchor = target.querySelector(".requests-list-file"); + await showTooltipOn(anchor); + + info("Tooltip was successfully opened for the image request."); + is( + toolboxDoc.querySelector(".tooltip-panel img").src, + TEST_IMAGE_DATA_URI, + "The tooltip's image content is displayed correctly." + ); + } + + /** + * Trigger a tooltip over an element by sending mousemove event. + * @return a promise that resolves when the tooltip is shown + */ + async function showTooltipOn(element) { + const win = element.ownerDocument.defaultView; + EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" }, win); + await waitUntil(() => toolboxDoc.querySelector(".tooltip-panel img")); + } + + /** + * Hide a tooltip on the {target} and verify that it was closed. + */ + async function hideTooltipAndVerify(target) { + // Hovering over the "method" column hides the tooltip. + const anchor = target.querySelector(".requests-list-method"); + const win = anchor.ownerDocument.defaultView; + EventUtils.synthesizeMouseAtCenter(anchor, { type: "mousemove" }, win); + + await waitUntil( + () => !toolboxDoc.querySelector(".tooltip-container.tooltip-visible") + ); + info("Tooltip was successfully closed."); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_image_cache.js b/devtools/client/netmonitor/test/browser_net_image_cache.js new file mode 100644 index 0000000000..55542dfebf --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_image_cache.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests image caches can be displayed in the network monitor + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(IMAGE_CACHE_URL, { + enableCache: true, + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const waitForEvents = waitForNetworkEvents(monitor, 2); + + // Using `BrowserTestUtils.loadURI` instead of `navigateTo` because + // `navigateTo` would use `gBrowser.reloadTab` to reload the tab + // when the current uri is the same as the one that is going to navigate. + // And the issue is that with `gBrowser.reloadTab`, `VALIDATE_ALWAYS` + // flag will be applied to the loadgroup, such that the sub resources + // are forced to be revalidated. So we use `BrowserTestUtils.loadURI` to + // avoid having `VALIDATE_ALWAYS` applied. + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + IMAGE_CACHE_URL + ); + await waitForEvents; + + const requests = document.querySelectorAll(".request-list-item"); + + // Though there are 3 test-image.png images on the page, only 1 request + // representing the images from the cache should be shown. Therefore we + // expect 2 requests all together (1 for html_image-cache.html and 1 for + // test-image.png) + is(requests.length, 2, "There should be 2 requests"); + + const requestData = { + uri: HTTPS_EXAMPLE_URL + "test-image.png", + details: { + status: 200, + statusText: "OK (cached)", + displayedStatus: "cached", + type: "png", + fullMimeType: "image/png", + }, + }; + + for (let i = 1; i < requests.length; ++i) { + const request = requests[i]; + + // mouseover is needed for tooltips to show up. + const requestsListStatus = request.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + + await verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[i], + "GET", + requestData.uri, + requestData.details + ); + } + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_initiator.js b/devtools/client/netmonitor/test/browser_net_initiator.js new file mode 100644 index 0000000000..94fa3a8624 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_initiator.js @@ -0,0 +1,291 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +const { + getUrlBaseName, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +/** + * Tests if request initiator is reported correctly. + */ + +const INITIATOR_FILE_NAME = "html_cause-test-page.html"; +const INITIATOR_URL = HTTPS_EXAMPLE_URL + INITIATOR_FILE_NAME; + +const EXPECTED_REQUESTS = [ + { + method: "GET", + url: INITIATOR_URL, + causeType: "document", + causeUri: null, + stack: false, + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "stylesheet_request", + causeType: "stylesheet", + causeUri: INITIATOR_URL, + stack: false, + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "img_request", + causeType: "img", + causeUri: INITIATOR_URL, + stack: false, + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "img_srcset_request", + causeType: "imageset", + causeUri: INITIATOR_URL, + stack: false, + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "xhr_request", + causeType: "xhr", + causeUri: INITIATOR_URL, + stack: [{ fn: "performXhrRequestCallback", file: INITIATOR_URL, line: 32 }], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "fetch_request", + causeType: "fetch", + causeUri: INITIATOR_URL, + stack: [{ fn: "performFetchRequest", file: INITIATOR_URL, line: 37 }], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "promise_fetch_request", + causeType: "fetch", + causeUri: INITIATOR_URL, + stack: [ + { + fn: "performPromiseFetchRequestCallback", + file: INITIATOR_URL, + line: 43, + }, + { + fn: "performPromiseFetchRequest", + file: INITIATOR_URL, + line: 42, + asyncCause: "promise callback", + }, + ], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "timeout_fetch_request", + causeType: "fetch", + causeUri: INITIATOR_URL, + stack: [ + { + fn: "performTimeoutFetchRequestCallback2", + file: INITIATOR_URL, + line: 50, + }, + { + fn: "performTimeoutFetchRequestCallback1", + file: INITIATOR_URL, + line: 49, + asyncCause: "setTimeout handler", + }, + ], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "favicon_request", + causeType: "img", + causeUri: INITIATOR_URL, + // the favicon request is triggered in FaviconLoader.sys.mjs module, it should + // NOT be shown in the stack (bug 1280266). For now we intentionally + // specify the file and the line number to be properly sorted. + // NOTE: The line number can be an arbitrary number greater than 0. + stack: [ + { + file: "resource:///modules/FaviconLoader.sys.mjs", + line: Number.MAX_SAFE_INTEGER, + }, + ], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "lazy_img_request", + causeType: "lazy-img", + causeUri: INITIATOR_URL, + stack: false, + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "lazy_img_srcset_request", + causeType: "lazy-imageset", + causeUri: INITIATOR_URL, + stack: false, + }, + { + method: "POST", + url: HTTPS_EXAMPLE_URL + "beacon_request", + causeType: "beacon", + causeUri: INITIATOR_URL, + stack: [{ fn: "performBeaconRequest", file: INITIATOR_URL, line: 82 }], + }, +]; + +add_task(async function () { + // the initNetMonitor function clears the network request list after the + // page is loaded. That's why we first load a bogus page from SIMPLE_URL, + // and only then load the real thing from INITIATOR_URL - we want to catch + // all the requests the page is making, not only the XHRs. + // We can't use about:blank here, because initNetMonitor checks that the + // page has actually made at least one request. + const { tab, monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, INITIATOR_URL); + + registerFaviconNotifier(tab.linkedBrowser); + + await wait; + + // For all expected requests + for (const [index, { stack }] of EXPECTED_REQUESTS.entries()) { + if (!stack) { + continue; + } + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll( + ".request-list-item .requests-list-initiator-lastframe" + )[index] + ); + + // Clicking on the initiator column should open the Stack Trace panel + const onStackTraceRendered = waitUntil(() => + document.querySelector("#stack-trace-panel .stack-trace .frame-link") + ); + await onStackTraceRendered; + } + + is( + store.getState().requests.requests.length, + EXPECTED_REQUESTS.length, + "All the page events should be recorded." + ); + + validateRequests(EXPECTED_REQUESTS, monitor); + + // Sort the requests by initiator and check the order + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-initiator-button") + ); + + const expectedOrder = EXPECTED_REQUESTS.sort(initiatorSortPredicate).map( + r => { + let isChromeFrames = false; + const lastFrameExists = !!r.stack; + let initiator = ""; + let lineNumber = ""; + if (lastFrameExists) { + const { file, line: _lineNumber } = r.stack[0]; + initiator = getUrlBaseName(file); + lineNumber = ":" + _lineNumber; + isChromeFrames = file.startsWith("resource:///"); + } + const causeStr = lastFrameExists ? " (" + r.causeType + ")" : r.causeType; + return { + initiatorStr: initiator + lineNumber + causeStr, + isChromeFrames, + }; + } + ); + + expectedOrder.forEach((expectedInitiator, i) => { + const request = getSortedRequests(store.getState())[i]; + let initiator; + // In cases of chrome frames, we shouldn't have stack. + if ( + request.cause.stacktraceAvailable && + !expectedInitiator.isChromeFrames + ) { + const { filename, lineNumber } = request.cause.lastFrame; + initiator = + getUrlBaseName(filename) + + ":" + + lineNumber + + " (" + + request.cause.type + + ")"; + } else { + initiator = request.cause.type; + } + + if (expectedInitiator.isChromeFrames) { + todo_is( + initiator, + expectedInitiator.initiatorStr, + `The request #${i} has the expected initiator after sorting` + ); + } else { + is( + initiator, + expectedInitiator.initiatorStr, + `The request #${i} has the expected initiator after sorting` + ); + } + }); + + await teardown(monitor); +}); + +// derived from devtools/client/netmonitor/src/utils/sort-predicates.js +function initiatorSortPredicate(first, second) { + const firstLastFrame = first.stack ? first.stack[0] : null; + const secondLastFrame = second.stack ? second.stack[0] : null; + + let firstInitiator = ""; + let firstInitiatorLineNumber = 0; + + if (firstLastFrame) { + firstInitiator = getUrlBaseName(firstLastFrame.file); + firstInitiatorLineNumber = firstLastFrame.line; + } + + let secondInitiator = ""; + let secondInitiatorLineNumber = 0; + + if (secondLastFrame) { + secondInitiator = getUrlBaseName(secondLastFrame.file); + secondInitiatorLineNumber = secondLastFrame.line; + } + + let result; + // if both initiators don't have a stack trace, compare their causes + if (!firstInitiator && !secondInitiator) { + result = compareValues(first.causeType, second.causeType); + } else if (!firstInitiator || !secondInitiator) { + // if one initiator doesn't have a stack trace but the other does, former should precede the latter + result = compareValues(firstInitiatorLineNumber, secondInitiatorLineNumber); + } else { + result = compareValues(firstInitiator, secondInitiator); + if (result === 0) { + result = compareValues( + firstInitiatorLineNumber, + secondInitiatorLineNumber + ); + } + } + return result; +} diff --git a/devtools/client/netmonitor/test/browser_net_internal-stylesheet.js b/devtools/client/netmonitor/test/browser_net_internal-stylesheet.js new file mode 100644 index 0000000000..098f2803aa --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_internal-stylesheet.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = EXAMPLE_URL + "html_internal-stylesheet.html"; + +/** + * Test for the stylesheet which is loaded as internal. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(TEST_URL, { + requestCount: 2, + }); + + const wait = waitForNetworkEvents(monitor, 2); + await reloadBrowser(); + await wait; + + const { store } = monitor.panelWin; + const requests = store.getState().requests.requests; + is( + requests.length, + 2, + "The number of requests state in the store is correct" + ); + + const styleSheetRequest = requests.find( + r => r.urlDetails.baseNameWithQuery === "internal-loaded.css" + ); + ok( + styleSheetRequest, + "The stylesheet which is loaded as internal is in the request" + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_json-b64.js b/devtools/client/netmonitor/test/browser_net_json-b64.js new file mode 100644 index 0000000000..82679d15ef --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json-b64.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if JSON responses encoded in base64 are handled correctly. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + const { tab, monitor } = await initNetMonitor(JSON_B64_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + let wait = waitForDOM(document, "#response-panel .data-header"); + const waitForPropsView = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + + store.dispatch(Actions.toggleNetworkDetails()); + + clickOnSidebarTab(document, "response"); + + await Promise.all([wait, waitForPropsView]); + + const tabpanel = document.querySelector("#response-panel"); + is( + tabpanel.querySelectorAll(".treeRow").length, + 1, + "There should be 1 json properties displayed in this tabpanel." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is( + labels[0].textContent, + "greeting", + "The first json property name was incorrect." + ); + is( + values[0].textContent, + `"This is a base 64 string."`, + "The first json property value was incorrect." + ); + + // Open the response payload section, it should hide the json section + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + const header = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(header, monitor); + await wait; + + is( + tabpanel.querySelector(".response-error-header") === null, + true, + "The response error header doesn't have the intended visibility." + ); + const jsonView = tabpanel.querySelector(".data-label") || {}; + is( + jsonView.textContent === L10N.getStr("jsonScopeName"), + true, + "The response json view has the intended visibility." + ); + is( + tabpanel.querySelector(".raw-data-toggle-input .devtools-checkbox-toggle") + .checked, + true, + "The raw response toggle should be on." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + false, + "The response editor has the intended visibility." + ); + is( + tabpanel.querySelector(".response-image-box") === null, + true, + "The response image box doesn't have the intended visibility." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_json-empty.js b/devtools/client/netmonitor/test/browser_net_json-empty.js new file mode 100644 index 0000000000..0179a97d49 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json-empty.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if empty JSON responses are properly displayed. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + JSON_EMPTY_URL + "?name=empty", + { requestCount: 1 } + ); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { L10N } = windowRequire("devtools/client/netmonitor/src/utils/l10n"); + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const onResponsePanelReady = waitForDOM( + document, + "#response-panel .data-header" + ); + + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + + await onResponsePanelReady; + + const codeMirrorReady = waitForDOM( + document, + "#response-panel .CodeMirror-code" + ); + + const header = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(header, monitor); + + await codeMirrorReady; + + const tabpanel = document.querySelector("#response-panel"); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + + is( + tabpanel.querySelector(".response-error-header") === null, + true, + "The response error header doesn't have the intended visibility." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + false, + "The response editor has the intended visibility." + ); + is( + tabpanel.querySelector(".response-image-box") === null, + true, + "The response image box doesn't have the intended visibility." + ); + + const jsonView = tabpanel.querySelector(".data-label") || {}; + is( + jsonView.textContent === L10N.getStr("jsonScopeName"), + true, + "The response json view has the intended visibility." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_json-long.js b/devtools/client/netmonitor/test/browser_net_json-long.js new file mode 100644 index 0000000000..4884e55b83 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json-long.js @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if very long JSON responses are handled correctly. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(JSON_LONG_URL, { + requestCount: 1, + }); + info("Starting test... "); + + // This is receiving over 80 KB of json and will populate over 6000 items + // in a variables view instance. Debug builds are slow. + requestLongerTimeout(4); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const requestItem = document.querySelector(".request-list-item"); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CONTENT_TYPE_SJS + "?fmt=json-long", + { + status: 200, + statusText: "OK", + type: "json", + fullMimeType: "text/json; charset=utf-8", + size: L10N.getFormatStr( + "networkMenu.size.kB", + L10N.numberWithDecimals(85975 / 1000, 2) + ), + time: true, + } + ); + + let wait = waitForDOM(document, "#response-panel .data-header"); + const waitForPropsView = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + + store.dispatch(Actions.toggleNetworkDetails()); + + clickOnSidebarTab(document, "response"); + + await Promise.all([wait, waitForPropsView]); + + // Scroll the properties view to the bottom + const lastItem = document.querySelector( + "#response-panel .properties-view tr.treeRow:last-child" + ); + lastItem.scrollIntoView(); + + testJsonInResposeTab(); + + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + const rawResponseToggle = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawResponseToggle, monitor); + await wait; + + testResponseTab(); + + await teardown(monitor); + + function testJsonInResposeTab() { + const tabpanel = document.querySelector("#response-panel"); + is( + tabpanel.querySelectorAll(".treeRow").length, + 2047, + "There should be 2047 json properties displayed in this tabpanel." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is( + labels[0].textContent, + "0", + "The first json property name was incorrect." + ); + is( + values[0].textContent, + 'Object { greeting: "Hello long string JSON!" }', + "The first json property value was incorrect." + ); + + is( + labels[1].textContent, + "1", + "The second json property name was incorrect." + ); + is( + values[1].textContent, + '"Hello long string JSON!"', + "The second json property value was incorrect." + ); + + const view = tabpanel.querySelector(".properties-view .treeTable"); + is(scrolledToBottom(view), true, "The view is not scrollable"); + } + + function testResponseTab() { + const tabpanel = document.querySelector("#response-panel"); + + is( + tabpanel.querySelector(".response-error-header") === null, + true, + "The response error header doesn't have the intended visibility." + ); + const jsonView = tabpanel.querySelector(".data-label") || {}; + is( + jsonView.textContent === L10N.getStr("jsonScopeName"), + true, + "The response json view has the intended visibility." + ); + is( + tabpanel.querySelector(".source-editor-mount").clientHeight !== 0, + true, + "The source editor container has visible height." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + false, + "The response editor has the intended visibility." + ); + is( + tabpanel.querySelector(".response-image-box") === null, + true, + "The response image box doesn't have the intended visibility." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + + is( + tabpanel.querySelector(".data-label").textContent, + L10N.getStr("jsonScopeName"), + "The json view section doesn't have the correct title." + ); + } + + function scrolledToBottom(element) { + return element.scrollTop + element.clientHeight >= element.scrollHeight; + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_json-malformed.js b/devtools/client/netmonitor/test/browser_net_json-malformed.js new file mode 100644 index 0000000000..0e29e07e9d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json-malformed.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if malformed JSON responses are handled correctly. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + const { tab, monitor } = await initNetMonitor(JSON_MALFORMED_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const requestItem = document.querySelector(".request-list-item"); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CONTENT_TYPE_SJS + "?fmt=json-malformed", + { + status: 200, + statusText: "OK", + type: "json", + fullMimeType: "text/json; charset=utf-8", + } + ); + + const wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await wait; + + const tabpanel = document.querySelector("#response-panel"); + is( + tabpanel.querySelector(".response-error-header") === null, + false, + "The response error header doesn't have the intended visibility." + ); + is( + tabpanel.querySelector(".response-error-header").textContent, + "SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data" + + " at line 1 column 40 of the JSON data", + "The response error header doesn't have the intended text content." + ); + is( + tabpanel.querySelector(".response-error-header").getAttribute("title"), + "SyntaxError: JSON.parse: unexpected non-whitespace character after JSON data" + + " at line 1 column 40 of the JSON data", + "The response error header doesn't have the intended tooltiptext attribute." + ); + const jsonView = tabpanel.querySelector(".tree-section .treeLabel") || {}; + is( + jsonView.textContent === L10N.getStr("jsonScopeName"), + false, + "The response json view doesn't have the intended visibility." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + false, + "The response editor has the intended visibility." + ); + is( + tabpanel.querySelector(".response-image-box") === null, + true, + "The response image box doesn't have the intended visibility." + ); + + is( + getCodeMirrorValue(monitor), + '{ "greeting": "Hello malformed JSON!" },', + "The text shown in the source editor is incorrect." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_json-nogrip.js b/devtools/client/netmonitor/test/browser_net_json-nogrip.js new file mode 100644 index 0000000000..6b00829f2f --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json-nogrip.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if JSON responses with property 'type' are correctly rendered. + * (Reps rendering JSON responses should use `noGrip=true`). + */ +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + JSON_BASIC_URL + "?name=nogrip", + { requestCount: 1 } + ); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + await performRequests(monitor, tab, 1); + + const onResponsePanelReady = waitForDOM( + document, + "#response-panel .data-header" + ); + + const onPropsViewReady = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await Promise.all([onResponsePanelReady, onPropsViewReady]); + + const tabpanel = document.querySelector("#response-panel"); + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is(labels[0].textContent, "obj", "The first json property name is correct."); + is( + values[0].textContent, + 'Object { type: "string" }', + "The first json property value is correct." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_json-null.js b/devtools/client/netmonitor/test/browser_net_json-null.js new file mode 100644 index 0000000000..ec040aaca1 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json-null.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if JSON responses containing null values are properly displayed. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(JSON_BASIC_URL + "?name=null", { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const { L10N } = windowRequire("devtools/client/netmonitor/src/utils/l10n"); + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const onResponsePanelReady = waitForDOM( + document, + "#response-panel .data-header" + ); + + const onPropsViewReady = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await Promise.all([onResponsePanelReady, onPropsViewReady]); + + const tabpanel = document.querySelector("#response-panel"); + is( + tabpanel.querySelectorAll(".raw-data-toggle").length, + 1, + "There should be 1 raw response toggle." + ); + is( + tabpanel.querySelectorAll(".treeRow").length, + 1, + "There should be 1 json properties displayed in this tabpanel." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is( + labels[0].textContent, + "greeting", + "The first json property name was incorrect." + ); + is( + values[0].textContent, + "null", + "The first json property value was incorrect." + ); + + const onCodeMirrorReady = waitForDOM( + document, + "#response-panel .CodeMirror-code" + ); + + const rawResponseToggle = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawResponseToggle, monitor); + + await onCodeMirrorReady; + + checkResponsePanelDisplaysJSON(); + + await teardown(monitor); + + /** + * Helper to assert that the response panel found in the provided document is currently + * showing a preview of a JSON object. + */ + function checkResponsePanelDisplaysJSON() { + const panel = document.querySelector("#response-panel"); + is( + panel.querySelector(".response-error-header") === null, + true, + "The response error header doesn't have the intended visibility." + ); + const jsonView = panel.querySelector(".data-label") || {}; + is( + jsonView.textContent === L10N.getStr("jsonScopeName"), + true, + "The response json view has the intended visibility." + ); + is( + panel.querySelector(".CodeMirror-code") === null, + false, + "The response editor has the intended visibility." + ); + is( + panel.querySelector(".response-image-box") === null, + true, + "The response image box doesn't have the intended visibility." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_json-xssi-protection.js b/devtools/client/netmonitor/test/browser_net_json-xssi-protection.js new file mode 100644 index 0000000000..6120e4e3bf --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json-xssi-protection.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if JSON responses and requests with XSSI protection sequences + * are handled correctly. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(JSON_XSSI_PROTECTION_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const wait = waitForDOM(document, "#response-panel .data-header"); + const waitForRawView = waitForDOM( + document, + "#response-panel .CodeMirror-code", + 1 + ); + + store.dispatch(Actions.toggleNetworkDetails()); + info("Opening response panel"); + clickOnSidebarTab(document, "response"); + + await Promise.all([wait, waitForRawView]); + + info( + "making sure response panel defaults to raw view and correctly displays payload" + ); + const codeLines = document.querySelector("#response-panel .CodeMirror-code"); + const firstLine = codeLines.firstChild; + const firstLineText = firstLine.querySelector("pre.CodeMirror-line span"); + is( + firstLineText.textContent, + ")]}'", + "XSSI protection sequence should be visibly in raw view" + ); + info("making sure XSSI notification box is not present in raw view"); + let notification = document.querySelector( + '.network-monitor #response-panel .notification[data-key="xssi-string-removed-info-box"]' + ); + ok(!notification, "notification should not be present in raw view"); + + const waitForPropsView = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + + info("switching to props view"); + const tabpanel = document.querySelector("#response-panel"); + const rawResponseToggle = tabpanel.querySelector("#raw-response-checkbox"); + clickElement(rawResponseToggle, monitor); + await waitForPropsView; + + is( + tabpanel.querySelectorAll(".treeRow").length, + 1, + "There should be 1 json property displayed in the response." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + info("Checking content of displayed json response"); + is(labels[0].textContent, "greeting", "The first key should be correct"); + is( + values[0].textContent, + `"Hello good XSSI protection"`, + "The first property should be correct" + ); + + info("making sure notification box is present and correct in props view"); + + notification = document.querySelector( + '.network-monitor #response-panel .notification[data-key="xssi-string-removed-info-box"] .notificationInner .messageText' + ); + + is( + notification.textContent, + "The string “)]}'\n” was removed from the beginning of the JSON shown below", + "The notification message is correct" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_json_custom_mime.js b/devtools/client/netmonitor/test/browser_net_json_custom_mime.js new file mode 100644 index 0000000000..1b8a0196cf --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json_custom_mime.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if JSON responses with unusal/custom MIME types are handled correctly. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(JSON_CUSTOM_MIME_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { L10N } = windowRequire("devtools/client/netmonitor/src/utils/l10n"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const requestItem = document.querySelector(".request-list-item"); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CONTENT_TYPE_SJS + "?fmt=json-custom-mime", + { + status: 200, + statusText: "OK", + type: "x-bigcorp-json", + fullMimeType: "text/x-bigcorp-json; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41), + time: true, + } + ); + + let wait = waitForDOM(document, "#response-panel .data-header"); + const waitForPropsView = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await Promise.all([wait, waitForPropsView]); + + testJsonSectionInResponseTab(); + + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + const rawResponseToggle = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawResponseToggle, monitor); + await wait; + + testResponseTab(); + + await teardown(monitor); + + function testJsonSectionInResponseTab() { + const tabpanel = document.querySelector("#response-panel"); + is( + tabpanel.querySelectorAll(".treeRow").length, + 1, + "There should be 1 json properties displayed in this tabpanel." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is( + labels[0].textContent, + "greeting", + "The first json property name was incorrect." + ); + is( + values[0].textContent, + `"Hello oddly-named JSON!"`, + "The first json property value was incorrect." + ); + } + + function testResponseTab() { + const tabpanel = document.querySelector("#response-panel"); + + is( + tabpanel.querySelector(".response-error-header") === null, + true, + "The response error header doesn't have the intended visibility." + ); + const jsonView = tabpanel.querySelector(".data-label") || {}; + is( + jsonView.textContent === L10N.getStr("jsonScopeName"), + true, + "The response json view has the intended visibility." + ); + is( + tabpanel.querySelector(".raw-data-toggle-input .devtools-checkbox-toggle") + .checked, + true, + "The raw response toggle should be on." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + false, + "The response editor has the intended visibility." + ); + is( + tabpanel.querySelector(".response-image-box") === null, + true, + "The response image box doesn't have the intended visibility." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_json_text_mime.js b/devtools/client/netmonitor/test/browser_net_json_text_mime.js new file mode 100644 index 0000000000..86fd4709d6 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_json_text_mime.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if JSON responses with unusal/custom MIME types are handled correctly. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(JSON_TEXT_MIME_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const requestItem = document.querySelector(".request-list-item"); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CONTENT_TYPE_SJS + "?fmt=json-text-mime", + { + status: 200, + statusText: "OK", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41), + time: true, + } + ); + + let wait = waitForDOM(document, "#response-panel .data-header"); + const waitForPropsView = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await Promise.all([wait, waitForPropsView]); + + testJsonSectionInResponseTab(); + + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + const rawResponseToggle = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawResponseToggle, monitor); + await wait; + + testResponseTab(); + + await teardown(monitor); + + function testJsonSectionInResponseTab() { + const tabpanel = document.querySelector("#response-panel"); + is( + tabpanel.querySelectorAll(".treeRow").length, + 1, + "There should be 1 json properties displayed in this tabpanel." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is( + labels[0].textContent, + "greeting", + "The first json property name was incorrect." + ); + is( + values[0].textContent, + `"Hello third-party JSON!"`, + "The first json property value was incorrect." + ); + } + + function testResponseTab() { + const tabpanel = document.querySelector("#response-panel"); + + is( + tabpanel.querySelector(".response-error-header") === null, + true, + "The response error header doesn't have the intended visibility." + ); + const jsonView = tabpanel.querySelector(".data-label") || {}; + is( + jsonView.textContent === L10N.getStr("jsonScopeName"), + true, + "The response json view has the intended visibility." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + false, + "The response editor has the intended visibility." + ); + is( + tabpanel.querySelector(".raw-data-toggle-input .devtools-checkbox-toggle") + .checked, + true, + "The raw response toggle should be on." + ); + is( + tabpanel.querySelector(".response-image-box") === null, + true, + "The response image box doesn't have the intended visibility." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_jsonp.js b/devtools/client/netmonitor/test/browser_net_jsonp.js new file mode 100644 index 0000000000..887e7c5bf3 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_jsonp.js @@ -0,0 +1,179 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if JSONP responses are handled correctly. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(JSONP_URL, { requestCount: 1 }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 2); + + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + } + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CONTENT_TYPE_SJS + "?fmt=jsonp&jsonp=$_0123Fun", + { + status: 200, + statusText: "OK", + type: "json", + fullMimeType: "text/json; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 41), + time: true, + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[1], + "GET", + CONTENT_TYPE_SJS + "?fmt=jsonp2&jsonp=$_4567Sad", + { + status: 200, + statusText: "OK", + type: "json", + fullMimeType: "text/json; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 54), + time: true, + } + ); + + info("Testing first request"); + let wait = waitForDOM(document, "#response-panel .data-header"); + let waitForPropsView = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await Promise.all([wait, waitForPropsView]); + + testJsonSectionInResponseTab(`"Hello JSONP!"`); + + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + let rawResponseToggle = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawResponseToggle, monitor); + await wait; + + testResponseTab("$_0123Fun"); + + info("Testing second request"); + + wait = waitForDOM(document, "#response-panel .data-header"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[1] + ); + + await wait; + + waitForPropsView = waitForDOM( + document, + "#response-panel .properties-view", + 1 + ); + rawResponseToggle = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawResponseToggle, monitor); + + await waitForPropsView; + + testJsonSectionInResponseTab(`"Hello weird JSONP!"`); + + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + rawResponseToggle = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawResponseToggle, monitor); + await wait; + + testResponseTab("$_4567Sad"); + + await teardown(monitor); + + function testJsonSectionInResponseTab(greeting) { + const tabpanel = document.querySelector("#response-panel"); + is( + tabpanel.querySelectorAll(".treeRow").length, + 1, + "There should be 1 json properties displayed in this tabpanel." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is( + labels[0].textContent, + "greeting", + "The first json property name was incorrect." + ); + is( + values[0].textContent, + greeting, + "The first json property value was incorrect." + ); + } + + function testResponseTab(func) { + const tabpanel = document.querySelector("#response-panel"); + + is( + tabpanel.querySelector(".response-error-header") === null, + true, + "The response error header doesn't have the intended visibility." + ); + is( + tabpanel.querySelector(".data-label").textContent, + L10N.getFormatStr("jsonpScopeName", func), + "The response json view has the intened visibility and correct title." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + false, + "The response editor has the intended visibility." + ); + is( + tabpanel.querySelector(".responseImageBox") === null, + true, + "The response image box doesn't have the intended visibility." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_large-response.js b/devtools/client/netmonitor/test/browser_net_large-response.js new file mode 100644 index 0000000000..5b74e27d84 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_large-response.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if very large response contents are just displayed as plain text. + */ + +const HTML_LONG_URL = CONTENT_TYPE_SJS + "?fmt=html-long"; + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + // This test could potentially be slow because over 100 KB of stuff + // is going to be requested and displayed in the source editor. + requestLongerTimeout(2); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [HTML_LONG_URL], + async function (url) { + content.wrappedJSObject.performRequests(1, url); + } + ); + await wait; + + const requestItem = document.querySelector(".request-list-item"); + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "GET", + CONTENT_TYPE_SJS + "?fmt=html-long", + { + status: 200, + statusText: "OK", + } + ); + + wait = waitForDOM(document, "#response-panel .data-header"); + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await wait; + + wait = waitForDOM(document, "#response-panel .CodeMirror-code"); + const payloadHeader = document.querySelector( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(payloadHeader, monitor); + await wait; + + ok( + getCodeMirrorValue(monitor).match(/^<p>/), + "The text shown in the source editor is incorrect." + ); + + info("Check that search input can be displayed"); + document.querySelector(".CodeMirror").CodeMirror.focus(); + synthesizeKeyShortcut("CmdOrCtrl+F"); + const searchInput = await waitFor(() => + document.querySelector(".CodeMirror input[type=search]") + ); + Assert.equal( + searchInput.ownerDocument.activeElement, + searchInput, + "search input is focused" + ); + + await teardown(monitor); + + // This test uses a lot of memory, so force a GC to help fragmentation. + info("Forcing GC after netmonitor test."); + Cu.forceGC(); +}); diff --git a/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js b/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js new file mode 100644 index 0000000000..318a644d5c --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_leak_on_tab_close.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that netmonitor doesn't leak windows on parent-side pages (bug 1285638) + */ + +add_task(async function () { + // Tell initNetMonitor to enable cache. Otherwise it will assert that there were more + // than zero network requests during the page load. But when loading about:config, + // there are none. + const { monitor } = await initNetMonitor("about:config", { + enableCache: true, + requestCount: 1, + }); + ok(monitor, "The network monitor was opened"); + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_new_request_panel.js b/devtools/client/netmonitor/test/browser_net_new_request_panel.js new file mode 100644 index 0000000000..dff66b2cdd --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_new_request_panel.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +/** + * Test if the New Request panel shows up as a expected when opened from the toolbar + */ + +add_task(async function () { + // Turn on the pref + await pushPref("devtools.netmonitor.features.newEditAndResend", true); + // Reset the storage for the persisted custom request + await asyncStorage.removeItem("devtools.netmonitor.customRequest"); + + const { monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + info("Open the HTTP Custom Panel through the toolbar button"); + let HTTPCustomRequestButton = document.querySelector( + "#netmonitor-toolbar-container .devtools-http-custom-request-icon" + ); + ok(HTTPCustomRequestButton, "The Toolbar button should be visible."); + const waitForPanels = waitForDOM( + document, + ".monitor-panel .network-action-bar" + ); + HTTPCustomRequestButton.click(); + await waitForPanels; + + is( + !!document.querySelector("#network-action-bar-HTTP-custom-request-panel"), + true, + "The 'New Request' header should be visible when the pref is true." + ); + is( + !!document.querySelector( + ".devtools-button.devtools-http-custom-request-icon.checked" + ), + true, + "The toolbar button should be highlighted" + ); + + info("if the default state is empty"); + is( + document.querySelector(".http-custom-method-value").value, + "GET", + "The method input should be 'GET' by default" + ); + is( + document.querySelector(".http-custom-url-value").textContent, + "", + "The URL input should be empty" + ); + const urlParametersValue = document.querySelectorAll( + "#http-custom-query .tabpanel-summary-container.http-custom-input" + ); + is(urlParametersValue.length, 0, "The URL Parameters input should be empty"); + const headersValue = document.querySelectorAll( + "#http-custom-headers .tabpanel-summary-container.http-custom-input" + ); + is(headersValue.length, 0, "The Headers input should be empty"); + is( + document.querySelector("#http-custom-postdata-value").textContent, + "", + "The Post body input should be empty" + ); + + // Turn off the pref + await pushPref("devtools.netmonitor.features.newEditAndResend", false); + info("Close the panel to update the interface after changing the pref"); + const closePanel = document.querySelector( + ".network-action-bar .tabs-navigation .sidebar-toggle" + ); + closePanel.click(); + + info("Check if the toolbar button is hidden when the pref is false"); + HTTPCustomRequestButton = document.querySelector( + "#netmonitor-toolbar-container .devtools-http-custom-request-icon" + ); + is( + !!HTTPCustomRequestButton, + false, + "The toolbar button should be hidden when the pref is false." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_new_request_panel_clear_button.js b/devtools/client/netmonitor/test/browser_net_new_request_panel_clear_button.js new file mode 100644 index 0000000000..78f4f8624d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_new_request_panel_clear_button.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +/** + * Test cleaning a custom request. + */ +add_task(async function () { + // Turn on the pref + await pushPref("devtools.netmonitor.features.newEditAndResend", true); + // Reset the storage for the persisted custom request + await asyncStorage.removeItem("devtools.netmonitor.customRequest"); + + const { monitor, tab } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + await performRequests(monitor, tab, 1); + + info("selecting first request"); + const firstRequestItem = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequestItem); + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequestItem); + + info("Opening the new request panel"); + const waitForPanels = waitUntil( + () => + document.querySelector(".http-custom-request-panel") && + document.querySelector("#http-custom-request-send-button").disabled === + false + ); + + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + await waitForPanels; + + const request = getSelectedRequest(store.getState()); + + // Check if the panel is updated with the content by the request clicked + const urlValue = document.querySelector(".http-custom-url-value"); + is( + urlValue.textContent, + request.url, + "The URL in the form should match the request we clicked" + ); + + info("Clicking on the clear button"); + document.querySelector("#http-custom-request-clear-button").click(); + is( + document.querySelector(".http-custom-method-value").value, + "GET", + "The method input should be 'GET' by default" + ); + is( + document.querySelector(".http-custom-url-value").textContent, + "", + "The URL input should be empty" + ); + const urlParametersValue = document.querySelectorAll( + "#http-custom-query .tabpanel-summary-container.http-custom-input" + ); + is(urlParametersValue.length, 0, "The URL Parameters input should be empty"); + const headersValue = document.querySelectorAll( + "#http-custom-headers .tabpanel-summary-container.http-custom-input" + ); + is(headersValue.length, 0, "The Headers input should be empty"); + is( + document.querySelector("#http-custom-postdata-value").textContent, + "", + "The Post body input should be empty" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_new_request_panel_content-length.js b/devtools/client/netmonitor/test/browser_net_new_request_panel_content-length.js new file mode 100644 index 0000000000..7d0bbc42c7 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_new_request_panel_content-length.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that the content length field is always updated when + * the message body changes. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + + info("Starting test... "); + + const { window, document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + await performRequests(monitor, tab, 1); + + info("Select the first request"); + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + await waitForHeaders; + + info("Open the first request in the request panel to resend"); + const waitForPanels = waitUntil( + () => + document.querySelector(".http-custom-request-panel") && + document.querySelector("#http-custom-request-send-button").disabled === + false + ); + + // Open the context menu and select resend menu item + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest); + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + + await waitForPanels; + + const contentLengthField = document.querySelector( + "#http-custom-content-length .http-custom-input-value" + ); + + is(contentLengthField.value, "15", "The content length is correct"); + + const messageBody = document.querySelector("#http-custom-postdata-value"); + messageBody.focus(); + EventUtils.sendString("bla=333&", window); + + await waitUntil(() => { + return contentLengthField.value == "23"; + }); + ok(true, "The content length is still correct"); + + const prevRequest = getSelectedRequest(store.getState()); + + info("Send the request"); + const waitUntilEventsDisplayed = waitForNetworkEvents(monitor, 1); + document.querySelector("#http-custom-request-send-button").click(); + await waitUntilEventsDisplayed; + + // Also make sure the selected request has switched to the new resent request + await waitUntil(() => getSelectedRequest(store.getState()) !== prevRequest); + + const newRequest = getSelectedRequest(store.getState()); + + // Wait for request headers to be available + await waitUntil(() => newRequest.requestHeaders?.headers.length); + + const contentLengthHeader = newRequest.requestHeaders.headers.find( + header => header.name == "Content-Length" + ); + + is( + contentLengthHeader.value, + "23", + "The content length of the resent request is correct" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_new_request_panel_context_menu.js b/devtools/client/netmonitor/test/browser_net_new_request_panel_context_menu.js new file mode 100644 index 0000000000..7aeb874852 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_new_request_panel_context_menu.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +/** + * Test if the New Request Panel shows up as a expected + * when opened from an existing request + */ + +add_task(async function () { + // Turn true the pref + await pushPref("devtools.netmonitor.features.newEditAndResend", true); + // Reset the storage for the persisted custom request + await asyncStorage.removeItem("devtools.netmonitor.customRequest"); + + const { tab, monitor } = await initNetMonitor(POST_DATA_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire, connector } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + await performRequests(monitor, tab, 2); + + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + const expectedURLQueryParams = [ + { + name: "foo", + value: "bar", + }, + { name: "baz", value: "42" }, + { name: "valueWithEqualSign", value: "hijk=123=mnop" }, + { name: "type", value: "urlencoded" }, + ]; + + info("selecting first request"); + const firstRequestItem = document.querySelectorAll(".request-list-item")[0]; + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequestItem); + await waitForHeaders; + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequestItem); + + ok( + getContextMenuItem(monitor, "request-list-context-resend-only"), + "The 'Resend' item is visible when there is a clicked request" + ); + + info("Opening the new request panel"); + const waitForPanels = waitUntil( + () => + document.querySelector(".http-custom-request-panel") && + document.querySelector("#http-custom-request-send-button").disabled === + false + ); + + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + await waitForPanels; + + is( + !!document.querySelector( + ".devtools-button.devtools-http-custom-request-icon.checked" + ), + true, + "The toolbar button should be highlighted" + ); + + const request = getSelectedRequest(store.getState()); + + // Verify if the default state contains the data from the request + const methodValue = document.querySelector(".http-custom-method-value"); + is( + methodValue.value, + request.method, + "The method in the form should match the request we clicked" + ); + + const urlValue = document.querySelector(".http-custom-url-value"); + is( + urlValue.textContent, + request.url, + "The URL in the form should match the request we clicked" + ); + + const urlParametersValues = document.querySelectorAll( + "#http-custom-query .tabpanel-summary-container.http-custom-input" + ); + is( + urlParametersValues.length, + 4, + "The URL Parameters length in the form should match the request we clicked" + ); + + for (let i = 0; i < urlParametersValues.length; i++) { + const { name, value } = expectedURLQueryParams[i]; + const [formName, formValue] = + urlParametersValues[i].querySelectorAll("textarea"); + is( + name, + formName.value, + "The query param name in the form should match the request we clicked" + ); + is( + value, + formValue.value, + "The query param value in the form should match the request we clicked" + ); + } + + const headersValues = document.querySelectorAll( + "#http-custom-headers .tabpanel-summary-container.http-custom-input" + ); + Assert.greaterOrEqual( + headersValues.length, + 6, + "The headers length in the form should match the request we clicked" + ); + + for (const { name, value } of request.requestHeaders.headers) { + const found = Array.from(headersValues).find(item => { + const [formName, formValue] = item.querySelectorAll("textarea"); + if (formName.value === name && formValue.value === value) { + return true; + } + return false; + }); + + ok(found, "The header was found in the form"); + } + + // Wait to the post body because it is only updated in the componentWillMount + const postValue = document.querySelector("#http-custom-postdata-value"); + await waitUntil(() => postValue.textContent !== ""); + is( + postValue.value, + request.requestPostData.postData.text, + "The Post body input value in the form should match the request we clicked" + ); + + info( + "Uncheck the header an make sure the header is removed from the new request" + ); + const headers = document.querySelectorAll( + "#http-custom-headers .tabpanel-summary-container.http-custom-input" + ); + + const lastHeader = Array.from(headers).pop(); + const checkbox = lastHeader.querySelector("input"); + checkbox.click(); + + info("Click on the button to send a new request"); + const waitUntilEventsDisplayed = waitForNetworkEvents(monitor, 1); + const buttonSend = document.querySelector("#http-custom-request-send-button"); + buttonSend.click(); + await waitUntilEventsDisplayed; + + const newRequestSelectedId = getSelectedRequest(store.getState()).id; + await connector.requestData(newRequestSelectedId, "requestHeaders"); + const updatedSelectedRequest = getSelectedRequest(store.getState()); + + let found = updatedSelectedRequest.requestHeaders.headers.some( + item => item.name == "My-header-2" && item.value == "my-value-2" + ); + + is( + found, + false, + "The header unchecked should not be found on the headers list" + ); + + info( + "Delete the header and make sure the header is removed in the custom request panel" + ); + const buttonDelete = lastHeader.querySelector("button"); + buttonDelete.click(); + + const headersValue = document.querySelectorAll( + "#http-custom-headers .tabpanel-summary-container.http-custom-input textarea" + ); + found = Array.from(headersValue).some( + item => item.name == "My-header-2" && item.value == "my-value-2" + ); + is(found, false, "The header delete should not be found on the headers form"); + + info( + "Change the request selected to make sure the request in the custom request panel does not change" + ); + const previousRequest = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, previousRequest); + + const urlValueChanged = document.querySelector(".http-custom-url-value"); + is( + urlValue.textContent, + urlValueChanged.textContent, + "The url should not change when click on a new request" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_new_request_panel_persisted_content.js b/devtools/client/netmonitor/test/browser_net_new_request_panel_persisted_content.js new file mode 100644 index 0000000000..509bbc0000 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_new_request_panel_persisted_content.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +/** + * Test if content is still persisted after the panel is closed + */ + +add_task(async function () { + // Turn true the pref + await pushPref("devtools.netmonitor.features.newEditAndResend", true); + // Reset the storage for the persisted custom request + await asyncStorage.removeItem("devtools.netmonitor.customRequest"); + + const { monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + info("open the left panel"); + let waitForPanels = waitForDOM( + document, + ".monitor-panel .network-action-bar" + ); + + let HTTPCustomRequestButton = document.querySelector( + "#netmonitor-toolbar-container .devtools-http-custom-request-icon" + ); + HTTPCustomRequestButton.click(); + await waitForPanels; + + info("Check if the panel is empty"); + is( + document.querySelector(".http-custom-url-value").value, + "", + "The URL input should be empty" + ); + + info("Add some content on the panel"); + const methodValue = document.querySelector("#http-custom-method-value"); + methodValue.value = "POST"; + methodValue.dispatchEvent(new Event("change", { bubbles: true })); + + const url = document.querySelector(".http-custom-url-value"); + url.focus(); + EventUtils.sendString("https://www.example.com"); + + info("Adding new parameters"); + const newParameterName = document.querySelector( + "#http-custom-query .map-add-new-inputs .http-custom-input-name" + ); + newParameterName.focus(); + EventUtils.sendString("My-param"); + + info("Adding new headers"); + const newHeaderName = document.querySelector( + "#http-custom-headers .map-add-new-inputs .http-custom-input-name" + ); + newHeaderName.focus(); + EventUtils.sendString("My-header"); + + const newHeaderValue = Array.from( + document.querySelectorAll( + "#http-custom-headers .http-custom-input .http-custom-input-value" + ) + ).pop(); + newHeaderValue.focus(); + EventUtils.sendString("my-value"); + + const postValue = document.querySelector("#http-custom-postdata-value"); + postValue.focus(); + EventUtils.sendString("{'Name': 'Value'}"); + + // Close the panel + const closePanel = document.querySelector( + ".network-action-bar .tabs-navigation .sidebar-toggle" + ); + closePanel.click(); + + // Open the panel again to see if the content is still there + waitForPanels = waitUntil( + () => + document.querySelector(".http-custom-request-panel") && + document.querySelector("#http-custom-request-send-button").disabled === + false + ); + + HTTPCustomRequestButton = document.querySelector( + "#netmonitor-toolbar-container .devtools-http-custom-request-icon" + ); + HTTPCustomRequestButton.click(); + await waitForPanels; + + is( + methodValue.value, + "POST", + "The content should still be there after the user close the panel and re-opened" + ); + + is( + url.value, + "https://www.example.com?My-param=", + "The url should still be there after the user close the panel and re-opened" + ); + + const [nameParam] = Array.from( + document.querySelectorAll( + "#http-custom-query .tabpanel-summary-container.http-custom-input textarea" + ) + ); + is( + nameParam.value, + "My-param", + "The Parameter name should still be there after the user close the panel and re-opened" + ); + + const [name, value] = Array.from( + document.querySelectorAll( + "#http-custom-headers .tabpanel-summary-container.http-custom-input textarea" + ) + ); + is( + name.value, + "My-header", + "The header name should still be there after the user close the panel and re-opened" + ); + is( + value.value, + "my-value", + "The header value should still be there after the user close the panel and re-opened" + ); + + is( + postValue.value, + "{'Name': 'Value'}", + "The content should still be there after the user close the panel and re-opened" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_new_request_panel_send_request.js b/devtools/client/netmonitor/test/browser_net_new_request_panel_send_request.js new file mode 100644 index 0000000000..656488de36 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_new_request_panel_send_request.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +/** + * Test sending a custom request. + */ +add_task(async function () { + // Turn on the pref + await pushPref("devtools.netmonitor.features.newEditAndResend", true); + // Reset the storage for the persisted custom request + await asyncStorage.removeItem("devtools.netmonitor.customRequest"); + + const { monitor } = await initNetMonitor(CORS_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire, connector } = monitor.panelWin; + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + // Build request with known information so that we can check later + const request = { + url: "https://test1.example.com" + CORS_SJS_PATH, + method: "POST", + headers: [ + { name: "Host", value: "fakehost.example.com" }, + { name: "User-Agent", value: "Testzilla" }, + { name: "Referer", value: "http://example.com/referrer" }, + { name: "Accept", value: "application/jarda" }, + { name: "Accept-Encoding", value: "compress, identity, funcoding" }, + { name: "Accept-Language", value: "cs-CZ" }, + ], + body: "Hello", + cause: { + loadingDocumentUri: "http://example.com", + stacktraceAvailable: true, + type: "xhr", + }, + }; + const waitUntilRequestDisplayed = waitForNetworkEvents(monitor, 1); + connector.networkCommand.sendHTTPRequest(request); + await waitUntilRequestDisplayed; + + info("selecting first request"); + const firstRequestItem = document.querySelectorAll(".request-list-item")[0]; + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequestItem); + await waitForHeaders; + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequestItem); + + info("Opening the new request panel"); + const waitForPanels = waitUntil( + () => + document.querySelector(".http-custom-request-panel") && + document.querySelector("#http-custom-request-send-button").disabled === + false + ); + + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + await waitForPanels; + + info( + "Change the request method to send a new custom request with a different method" + ); + const methodValue = document.querySelector("#http-custom-method-value"); + methodValue.value = "PUT"; + methodValue.dispatchEvent(new Event("change", { bubbles: true })); + + info("Change the URL to send a custom request with a different URL"); + const urlValue = document.querySelector(".http-custom-url-value"); + urlValue.focus(); + urlValue.value = ""; + EventUtils.sendString(`${request.url}?hello=world`); + + info("Check if the parameter section was updated"); + is( + document.querySelectorAll( + "#http-custom-query .tabpanel-summary-container.http-custom-input" + ).length, + 1, + "The parameter section should be updated" + ); + + info("Adding new headers"); + const newHeaderName = document.querySelector( + "#http-custom-headers .map-add-new-inputs .http-custom-input-name" + ); + newHeaderName.focus(); + EventUtils.sendString("My-header"); + + const newHeaderValue = Array.from( + document.querySelectorAll( + "#http-custom-headers .http-custom-input .http-custom-input-value" + ) + ).pop(); + newHeaderValue.focus(); + EventUtils.sendString("my-value"); + + const postValue = document.querySelector("#http-custom-postdata-value"); + postValue.focus(); + postValue.value = ""; + EventUtils.sendString("{'name': 'value'}"); + + // Close the details panel to see if after sending a new request + // this request will be selected by default and + // if the deitails panel will be open automatically. + const waitForDetailsPanelToClose = waitUntil( + () => !document.querySelector(".monitor-panel .network-details-bar") + ); + store.dispatch(Actions.toggleNetworkDetails()); + await waitForDetailsPanelToClose; + + info("Click on the button to send a new request"); + const waitUntilEventsDisplayed = waitForNetworkEvents(monitor, 1); + const buttonSend = document.querySelector("#http-custom-request-send-button"); + buttonSend.click(); + await waitUntilEventsDisplayed; + + const newRequestSelectedId = getSelectedRequest(store.getState()).id; + await connector.requestData(newRequestSelectedId, "requestPostData"); + const updatedSelectedRequest = getSelectedRequest(store.getState()); + is(updatedSelectedRequest.method, "PUT", "The request has the right method"); + is( + updatedSelectedRequest.url, + urlValue.value, + "The request has the right URL" + ); + + const found = updatedSelectedRequest.requestHeaders.headers.some( + item => item.name === "My-header" && item.value === "my-value" + ); + + is(found, true, "The header was found in the form"); + + is( + updatedSelectedRequest.requestPostData.postData.text, + "{'name': 'value'}", + "The request has the right post body" + ); + + info("Check that all growing textareas provide a title tooltip"); + const textareas = [ + ...document.querySelectorAll("#http-custom-headers .auto-growing-textarea"), + ]; + for (const textarea of textareas) { + is( + textarea.title, + textarea.dataset.replicatedValue, + "Title tooltip is set to the expected value" + ); + } + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_new_request_panel_sync_url_params.js b/devtools/client/netmonitor/test/browser_net_new_request_panel_sync_url_params.js new file mode 100644 index 0000000000..1d7b51f3d4 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_new_request_panel_sync_url_params.js @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const asyncStorage = require("resource://devtools/shared/async-storage.js"); + +/** + * Test to check the sync between URL parameters and the parameters section + */ + +add_task(async function () { + // Turn on the pref + await pushPref("devtools.netmonitor.features.newEditAndResend", true); + // Reset the storage for the persisted custom request + await asyncStorage.removeItem("devtools.netmonitor.customRequest"); + + const { tab, monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + await performRequests(monitor, tab, 1); + + info("selecting first request"); + const firstRequestItem = document.querySelectorAll(".request-list-item")[0]; + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequestItem); + await waitForHeaders; + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequestItem); + + info("Opening the new request panel"); + const waitForPanels = waitForDOM( + document, + ".monitor-panel .network-action-bar" + ); + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + await waitForPanels; + + const queryScenarios = [ + { + queryString: "", + expectedParametersSize: 0, + expectedParameters: [], + }, + { + queryString: "?", + expectedParametersSize: 0, + expectedParameters: [], + }, + { + queryString: "?a", + expectedParametersSize: 1, + expectedParameters: [{ name: "a", value: "" }], + }, + { + queryString: "?a=", + expectedParametersSize: 1, + expectedParameters: [{ name: "a", value: "" }], + }, + { + queryString: "?a=3", + expectedParametersSize: 1, + expectedParameters: [{ name: "a", value: "3" }], + }, + { + queryString: "?a=3&", + expectedParametersSize: 2, + expectedParameters: [ + { name: "a", value: "3" }, + { name: "", value: "" }, + ], + }, + { + queryString: "?a=3&b=4", + expectedParametersSize: 2, + expectedParameters: [ + { name: "a", value: "3" }, + { name: "b", value: "4" }, + ], + }, + ]; + + for (const sceanario of queryScenarios) { + assertQueryScenario(document, sceanario); + } + + info("Adding new parameters by query parameters section"); + const newParameterName = document.querySelector( + "#http-custom-query .map-add-new-inputs .http-custom-input-name" + ); + newParameterName.focus(); + EventUtils.sendString("My-param"); + + is( + document.querySelector("#http-custom-url-value").value, + `${HTTPS_CUSTOM_GET_URL}?a=3&b=4&My-param=`, + "The URL should be updated" + ); + + const newParameterValue = Array.from( + document.querySelectorAll( + "#http-custom-query .http-custom-input .http-custom-input-value" + ) + ).pop(); + newParameterValue.focus(); + EventUtils.sendString("my-value"); + + // Check if the url is updated + is( + document.querySelector("#http-custom-url-value").value, + `${HTTPS_CUSTOM_GET_URL}?a=3&b=4&My-param=my-value`, + "The URL should be updated" + ); + + info("Adding new parameters by query parameters section"); + is( + document.querySelectorAll( + "#http-custom-query .tabpanel-summary-container.http-custom-input" + ).length, + 3, + "The parameter section should be have 3 elements" + ); + + info( + "Uncheck the parameter an make sure the parameter is removed from the new url" + ); + const params = document.querySelectorAll( + "#http-custom-query .tabpanel-summary-container.http-custom-input" + ); + + const lastParam = Array.from(params).pop(); + const checkbox = lastParam.querySelector("input"); + checkbox.click(); + + // Check if the url is updated after uncheck one parameter through the parameter section + is( + document.querySelector("#http-custom-url-value").value, + `${HTTPS_CUSTOM_GET_URL}?a=3&b=4`, + "The URL should be updated" + ); + + await teardown(monitor); +}); + +function assertQueryScenario( + document, + { queryString, expectedParametersSize, expectedParameters } +) { + info(`Assert that "${queryString}" shows in the list properly`); + const urlValue = document.querySelector(".http-custom-url-value"); + urlValue.focus(); + urlValue.value = ""; + + const newURL = HTTPS_CUSTOM_GET_URL + queryString; + EventUtils.sendString(newURL); + + is( + document.querySelectorAll( + "#http-custom-query .tabpanel-summary-container.http-custom-input" + ).length, + expectedParametersSize, + `The parameter section should have ${expectedParametersSize} elements` + ); + + // Check if the parameter name and value are what we expect + const parameterNames = document.querySelectorAll( + "#http-custom-query .http-custom-input-and-map-form .http-custom-input-name" + ); + const parameterValues = document.querySelectorAll( + "#http-custom-query .http-custom-input-and-map-form .http-custom-input-value" + ); + + for (let i = 0; i < expectedParameters.length; i++) { + is( + parameterNames[i].value, + expectedParameters[i].name, + "The query param name in the form should match on the URL" + ); + is( + parameterValues[i].value, + expectedParameters[i].value, + "The query param value in the form should match on the URL" + ); + } +} diff --git a/devtools/client/netmonitor/test/browser_net_offline_mode.js b/devtools/client/netmonitor/test/browser_net_offline_mode.js new file mode 100644 index 0000000000..9caa460d61 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_offline_mode.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test network throttling `offline` mode + +"use strict"; + +requestLongerTimeout(2); + +const { + PROFILE_CONSTANTS, +} = require("resource://devtools/client/shared/components/throttling/profiles.js"); + +add_task(async function () { + await pushPref("devtools.cache.disabled", true); + + const { tab, monitor, toolbox } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document } = monitor.panelWin; + + await selectThrottle(document, monitor, toolbox, PROFILE_CONSTANTS.OFFLINE); + assertCurrentThrottleSelected(document, PROFILE_CONSTANTS.OFFLINE); + + const offlineEventFired = SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + return content.wrappedJSObject.hasOfflineEventFired(); + } + ); + + ok(offlineEventFired, "The offline event on the page fired"); + + // As the browser is offline, load event won't fire + await reloadBrowser({ waitForLoad: false }); + + await assertNavigatorOnlineInConsole(toolbox, "false"); + await assertPageIsOffline(); + + await selectThrottle( + document, + monitor, + toolbox, + PROFILE_CONSTANTS.REGULAR_4G_LTE + ); + assertCurrentThrottleSelected(document, PROFILE_CONSTANTS.REGULAR_4G_LTE); + + await reloadBrowser(); + + await assertNavigatorOnlineInConsole(toolbox, "true"); + await assertPageIsOnline(); + + await teardown(monitor); +}); + +async function selectThrottle(document, monitor, toolbox, profileId) { + info(`Selecting the '${profileId}' profile`); + document.getElementById("network-throttling-menu").click(); + // Throttling menu items cannot be retrieved by id so we can't use getContextMenuItem + // here. Instead use querySelector on the toolbox top document, where the context menu + // will be rendered. + const item = toolbox.topWindow.document.querySelector( + "menuitem[label='" + profileId + "']" + ); + await BrowserTestUtils.waitForPopupEvent(item.parentNode, "shown"); + item.parentNode.activateItem(item); + await monitor.panelWin.api.once(TEST_EVENTS.THROTTLING_CHANGED); +} + +function assertCurrentThrottleSelected(document, expectedProfile) { + is( + document.querySelector("#network-throttling-menu .title").innerText, + expectedProfile, + `The '${expectedProfile}' throttle profile is correctly selected` + ); +} + +function assertPageIsOffline() { + // This is an error page. + return SpecialPowers.spawn( + gBrowser.selectedTab.linkedBrowser, + [HTTPS_CUSTOM_GET_URL], + function (uri) { + is( + content.document.documentURI.substring(0, 27), + "about:neterror?e=netOffline", + "Document URI is the error page." + ); + + // But location bar should show the original request. + is(content.location.href, uri, "Docshell URI is the original URI."); + } + ); +} + +function assertPageIsOnline() { + // This is an error page. + return SpecialPowers.spawn( + gBrowser.selectedTab.linkedBrowser, + [HTTPS_CUSTOM_GET_URL], + function (uri) { + is(content.document.documentURI, uri, "Document URI is the original URI"); + + // But location bar should show the original request. + is(content.location.href, uri, "Docshell URI is the original URI."); + } + ); +} + +async function assertNavigatorOnlineInConsole(toolbox, expectedResultValue) { + const input = "navigator.onLine"; + info(`Assert the value of '${input}' in the console`); + await toolbox.openSplitConsole(); + const { hud } = await toolbox.getPanel("webconsole"); + hud.setInputValue(input); + return waitForEagerEvaluationResult(hud, expectedResultValue); +} diff --git a/devtools/client/netmonitor/test/browser_net_open_in_debugger.js b/devtools/client/netmonitor/test/browser_net_open_in_debugger.js new file mode 100644 index 0000000000..0ca96ffaa8 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_open_in_debugger.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the 'Open in debugger' feature + */ + +add_task(async function () { + const { tab, monitor, toolbox } = await initNetMonitor( + CONTENT_TYPE_WITHOUT_CACHE_URL, + { requestCount: 1 } + ); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + // Avoid async processing + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[2] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[2] + ); + const onDebuggerReady = toolbox.once("jsdebugger-ready"); + await selectContextMenuItem(monitor, "request-list-context-open-in-debugger"); + await onDebuggerReady; + + ok(true, "Debugger has been open"); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js b/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js new file mode 100644 index 0000000000..9592c74851 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the 'Open in debugger' feature + */ + +add_task(async function () { + const { tab, monitor, toolbox } = await initNetMonitor( + CONTENT_TYPE_WITHOUT_CACHE_URL, + { requestCount: 1 } + ); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + // Avoid async processing + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[1] + ); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[1] + ); + + const onStyleEditorReady = toolbox.once("styleeditor-ready"); + await selectContextMenuItem( + monitor, + "request-list-context-open-in-style-editor" + ); + await onStyleEditorReady; + + ok(true, "Style Editor has been open"); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js new file mode 100644 index 0000000000..db96c350ca --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if Open in new tab works by ContextMenu. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(OPEN_REQUEST_IN_TAB_URL, { + requestCount: 1, + }); + info("Starting test..."); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + let newTab; + + store.dispatch(Actions.batchEnable(false)); + + // Post data may be fetched by the Header panel, + // so set the Timings panel as the new default. + store.getState().ui.detailsPanelSelectedTab = "timings"; + + // Open GET request in new tab + await performRequest(monitor, tab, "GET"); + newTab = await openLastRequestInTab(); + await checkTabResponse(newTab, "GET"); + gBrowser.removeCurrentTab(); + + // Open POST request in new tab + await performRequest( + monitor, + tab, + "POST", + "application/x-www-form-urlencoded", + "foo=bar&baz=42" + ); + newTab = await openLastRequestInTab(); + await checkTabResponse( + newTab, + "POST", + "application/x-www-form-urlencoded", + "foo=bar&baz=42" + ); + gBrowser.removeCurrentTab(); + + // Open POST application/json request in new tab + await performRequest( + monitor, + tab, + "POST", + "application/json", + '{"foo":"bar"}' + ); + newTab = await openLastRequestInTab(); + await checkTabResponse(newTab, "POST", "application/json", '{"foo":"bar"}'); + gBrowser.removeCurrentTab(); + + await teardown(monitor); + + // OpenLastRequestInTab by ContextMenu + async function openLastRequestInTab() { + const requestItems = document.querySelectorAll(".request-list-item"); + const lastRequest = requestItems[requestItems.length - 1]; + EventUtils.sendMouseEvent({ type: "mousedown" }, lastRequest); + EventUtils.sendMouseEvent({ type: "contextmenu" }, lastRequest); + + const onTabOpen = once(gBrowser.tabContainer, "TabOpen", false); + await selectContextMenuItem(monitor, "request-list-context-newtab"); + await onTabOpen; + info("A new tab has been opened"); + + const awaitedTab = gBrowser.selectedTab; + await BrowserTestUtils.browserLoaded(awaitedTab.linkedBrowser); + info("The tab load completed"); + + return awaitedTab; + } +}); + +/** + * Tests if Open in new tab works by DoubleClick RequestItem. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(OPEN_REQUEST_IN_TAB_URL, { + requestCount: 1, + }); + info("Starting test..."); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + let newTab; + + store.dispatch(Actions.batchEnable(false)); + + // Post data may be fetched by the Header panel, + // so set the Timings panel as the new default. + store.getState().ui.detailsPanelSelectedTab = "timings"; + + // Open GET request in new tab + await performRequest(monitor, tab, "GET"); + newTab = await openLastRequestInTab(); + await checkTabResponse(newTab, "GET"); + gBrowser.removeCurrentTab(); + + // Open POST request in new tab + await performRequest( + monitor, + tab, + "POST", + "application/x-www-form-urlencoded", + "foo=bar&baz=42" + ); + newTab = await openLastRequestInTab(); + await checkTabResponse( + newTab, + "POST", + "application/x-www-form-urlencoded", + "foo=bar&baz=42" + ); + gBrowser.removeCurrentTab(); + + // Open POST application/json request in new tab + await performRequest( + monitor, + tab, + "POST", + "application/json", + '{"foo":"bar"}' + ); + newTab = await openLastRequestInTab(); + await checkTabResponse(newTab, "POST", "application/json", '{"foo":"bar"}'); + gBrowser.removeCurrentTab(); + + await teardown(monitor); + + // OpenLastRequestInTab by DoubleClick + async function openLastRequestInTab() { + const requestItems = document.querySelectorAll(".request-list-item"); + const lastRequest = requestItems[requestItems.length - 1]; + + const onTabOpen = once(gBrowser.tabContainer, "TabOpen", false); + EventUtils.sendMouseEvent({ type: "dblclick" }, lastRequest); + await onTabOpen; + info("A new tab has been opened"); + + const awaitedTab = gBrowser.selectedTab; + await BrowserTestUtils.browserLoaded(awaitedTab.linkedBrowser); + info("The tab load completed"); + + return awaitedTab; + } +}); + +/** + * Tests if Open in new tab works by middle click RequestItem. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(OPEN_REQUEST_IN_TAB_URL, { + requestCount: 1, + }); + const MIDDLE_MOUSE_BUTTON = 1; + info("Starting test..."); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + let newTab; + + store.dispatch(Actions.batchEnable(false)); + + // Post data may be fetched by the Header panel, + // so set the Timings panel as the new default. + store.getState().ui.detailsPanelSelectedTab = "timings"; + + // Open GET request in new tab + await performRequest(monitor, tab, "GET"); + newTab = await openLastRequestInTab(); + await checkTabResponse(newTab, "GET"); + gBrowser.removeCurrentTab(); + + // Open POST request in new tab + await performRequest( + monitor, + tab, + "POST", + "application/x-www-form-urlencoded", + "foo=bar&baz=42" + ); + newTab = await openLastRequestInTab(); + await checkTabResponse( + newTab, + "POST", + "application/x-www-form-urlencoded", + "foo=bar&baz=42" + ); + gBrowser.removeCurrentTab(); + + // Open POST application/json request in new tab + await performRequest( + monitor, + tab, + "POST", + "application/json", + '{"foo":"bar"}' + ); + newTab = await openLastRequestInTab(); + await checkTabResponse(newTab, "POST", "application/json", '{"foo":"bar"}'); + gBrowser.removeCurrentTab(); + + await teardown(monitor); + + // OpenLastRequestInTab by middle click + async function openLastRequestInTab() { + const requestItems = document.querySelectorAll(".request-list-item"); + const lastRequest = requestItems[requestItems.length - 1]; + + const onTabOpen = once(gBrowser.tabContainer, "TabOpen", false); + EventUtils.sendMouseEvent( + { type: "mousedown", button: MIDDLE_MOUSE_BUTTON }, + lastRequest + ); + await onTabOpen; + info("A new tab has been opened"); + + const awaitedTab = gBrowser.selectedTab; + await BrowserTestUtils.browserLoaded(awaitedTab.linkedBrowser); + info("The tab load completed"); + + return awaitedTab; + } +}); + +async function performRequest(monitor, tab, method, contentType, payload) { + const wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [[method, contentType, payload]], + async function ([method_, contentType_, payload_]) { + content.wrappedJSObject.performRequest(method_, contentType_, payload_); + } + ); + await wait; + info("Performed request to test server"); +} + +async function checkTabResponse(checkedTab, method, contentType, payload) { + await SpecialPowers.spawn( + checkedTab.linkedBrowser, + [[method, contentType, payload]], + async function ([method_, contentType_, payload_]) { + const { body } = content.wrappedJSObject.document; + const expected = [method_, contentType_, payload_].join("\n"); + info("Response from the server:" + body.innerHTML.replace(/\n/g, "\\n")); + ok( + body.innerHTML.includes(expected), + "Tab method and data match original request" + ); + } + ); +} diff --git a/devtools/client/netmonitor/test/browser_net_pane-collapse.js b/devtools/client/netmonitor/test/browser_net_pane-collapse.js new file mode 100644 index 0000000000..465b4740ac --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_pane-collapse.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the network monitor panes collapse properly. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { Prefs } = windowRequire("devtools/client/netmonitor/src/utils/prefs"); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + ok( + !document.querySelector(".network-details-bar") && + !document.querySelector(".sidebar-toggle"), + "The details panel should initially be hidden." + ); + + store.dispatch(Actions.toggleNetworkDetails()); + + is( + ~~document.querySelector(".network-details-bar").clientWidth, + Prefs.networkDetailsWidth, + "The details panel has an incorrect width." + ); + ok( + document.querySelector(".network-details-bar") && + document.querySelector(".sidebar-toggle"), + "The details panel should at this point be visible." + ); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".sidebar-toggle") + ); + + ok( + !document.querySelector(".network-details-bar") && + !document.querySelector(".sidebar-toggle"), + "The details panel should not be visible after collapsing." + ); + + store.dispatch(Actions.toggleNetworkDetails()); + + is( + ~~document.querySelector(".network-details-bar").clientWidth, + Prefs.networkDetailsWidth, + "The details panel has an incorrect width after uncollapsing." + ); + ok( + document.querySelector(".network-details-bar") && + document.querySelector(".sidebar-toggle"), + "The details panel should be visible again after uncollapsing." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_pane-network-details.js b/devtools/client/netmonitor/test/browser_net_pane-network-details.js new file mode 100644 index 0000000000..f5a11f9b4d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_pane-network-details.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the action of request details panel when filters are applied. + * If there are any visible requests, the first request from the + * list of visible requests should be displayed in the network + * details panel + * If there are no visible requests the panel should remain closed + */ + +const REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS = [ + { + url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample", + }, + { url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" }, + { url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" }, + { url: "sjs_content-type-test-server.sjs?fmt=font" }, + { url: "sjs_content-type-test-server.sjs?fmt=image" }, + { url: "sjs_content-type-test-server.sjs?fmt=audio" }, + { url: "sjs_content-type-test-server.sjs?fmt=video" }, + { url: "sjs_content-type-test-server.sjs?fmt=flash" }, + { url: "sjs_content-type-test-server.sjs?fmt=ws" }, +]; + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSelectedRequest, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + function setFreetextFilter(value) { + store.dispatch(Actions.setRequestFilterText(value)); + } + + info("Starting test... "); + + const wait = waitForNetworkEvents(monitor, 9); + await performRequestsInContent(REQUESTS_WITH_MEDIA_AND_FLASH_AND_WS); + await wait; + + info("Test with the first request in the list visible"); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + testDetailsPanel(true, 0); + + info("Test with first request in the list not visible"); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-js-button") + ); + testFilterButtons(monitor, "js"); + testDetailsPanel(true, 2); + + info( + "Test with no request in the list visible i.e. no request match the filters" + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-all-button") + ); + setFreetextFilter("foobar"); + // The network details panel should not open as there are no available requests visible + testDetailsPanel(false); + + await teardown(monitor); + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } + + function testDetailsPanel(shouldPanelOpen, selectedItemIndex = 0) { + // Expected default state should be panel closed + ok( + !document.querySelector(".sidebar-toggle"), + "The pane toggle button should not be visible." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The details pane should still be hidden." + ); + is( + getSelectedRequest(store.getState()), + undefined, + "There should still be no selected item in the requests menu." + ); + + // Trigger the details panel toggle action + store.dispatch(Actions.toggleNetworkDetails()); + + const toggleButton = document.querySelector(".sidebar-toggle"); + + if (shouldPanelOpen) { + is( + toggleButton.classList.contains("pane-collapsed"), + false, + "The pane toggle button should now indicate that the details pane is " + + "not collapsed anymore after being pressed." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The details pane should not be hidden after toggle button was pressed." + ); + isnot( + getSelectedRequest(store.getState()), + undefined, + "There should be a selected item in the requests menu." + ); + is( + getSelectedIndex(store.getState()), + selectedItemIndex, + `The item index ${selectedItemIndex} should be selected in the requests menu.` + ); + // Close the panel + EventUtils.sendMouseEvent({ type: "click" }, toggleButton); + } else { + ok(!toggleButton, "The pane toggle button should be not visible."); + is( + !!document.querySelector(".network-details-bar"), + false, + "The details pane should still be hidden." + ); + is( + getSelectedRequest(store.getState()), + undefined, + "There should still be no selected item in the requests menu." + ); + } + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_pane-toggle.js b/devtools/client/netmonitor/test/browser_net_pane-toggle.js new file mode 100644 index 0000000000..ab6cb63740 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_pane-toggle.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if toggling the details pane works as expected. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSelectedRequest, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + ok( + !document.querySelector(".sidebar-toggle"), + "The pane toggle button should not be visible." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The details pane should be hidden when the frontend is opened." + ); + is( + getSelectedRequest(store.getState()), + undefined, + "There should be no selected item in the requests menu." + ); + + const networkEvent = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await networkEvent; + + ok( + !document.querySelector(".sidebar-toggle"), + "The pane toggle button should not be visible after the first request." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The details pane should still be hidden after the first request." + ); + is( + getSelectedRequest(store.getState()), + undefined, + "There should still be no selected item in the requests menu." + ); + + store.dispatch(Actions.toggleNetworkDetails()); + + const toggleButton = document.querySelector(".sidebar-toggle"); + + is( + toggleButton.classList.contains("pane-collapsed"), + false, + "The pane toggle button should now indicate that the details pane is " + + "not collapsed anymore." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The details pane should not be hidden after toggle button was pressed." + ); + isnot( + getSelectedRequest(store.getState()), + undefined, + "There should be a selected item in the requests menu." + ); + is( + getSelectedIndex(store.getState()), + 0, + "The first item should be selected in the requests menu." + ); + + EventUtils.sendMouseEvent({ type: "click" }, toggleButton); + + is( + !!document.querySelector(".network-details-bar"), + false, + "The details pane should now be hidden after the toggle button was pressed again." + ); + is( + getSelectedRequest(store.getState()), + undefined, + "There should now be no selected item in the requests menu." + ); + + await teardown(monitor); + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_params_sorted.js b/devtools/client/netmonitor/test/browser_net_params_sorted.js new file mode 100644 index 0000000000..6008909889 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_params_sorted.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether keys in Params panel are sorted. + */ +add_task(async function () { + const { tab, monitor } = await initNetMonitor(POST_ARRAY_DATA_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + const wait = waitForDOM(document, ".headers-overview"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + clickOnSidebarTab(document, "request"); + + // The Params panel should render the following + // POSTed JSON data structure: + // + // ▼ JSON + // ▼ watches: […] + // 0: hello + // 1: how + // 2: are + // 3: you + // ▼ 4: {…} + // a: 10 + // ▼ b: […] + // 0: "a" + // 1: "c" + // 2: "b" + // c: 15 + const expectedKeys = [ + "watches\t[…]", + `0\t"hello"`, + `1\t"how"`, + `2\t"are"`, + `3\t"you"`, + "4\t{…}", + "a\t10", + "b\t[…]", + `0\t"a"`, + `1\t"c"`, + `2\t"b"`, + "c\t15", + ]; + + const waitForTreeRow = waitForDOM( + document, + ".treeTable .treeRow", + expectedKeys.length + ); + await waitForTreeRow; + const actualKeys = document.querySelectorAll(".treeTable .treeRow"); + + for (let i = 0; i < actualKeys.length; i++) { + const text = actualKeys[i].innerText.trim(); + is( + text, + expectedKeys[i], + "Actual value " + + text + + " is equal to the " + + "expected value " + + expectedKeys[i] + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_pause.js b/devtools/client/netmonitor/test/browser_net_pause.js new file mode 100644 index 0000000000..6ed01efe27 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_pause.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the pause/resume button works. + */ +add_task(async function () { + const { tab, monitor, toolbox } = await initNetMonitor(PAUSE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const pauseButton = document.querySelector(".requests-list-pause-button"); + + store.dispatch(Actions.batchEnable(false)); + + // Make sure we start in a sane state. + assertRequestCount(store, 0); + + // Load one request and assert it shows up in the list. + await performRequestAndWait(tab, monitor, SIMPLE_URL + "?id=1"); + assertRequestCount(store, 1); + + let noRequest = true; + monitor.panelWin.api.once(TEST_EVENTS.NETWORK_EVENT, () => { + noRequest = false; + }); + + monitor.panelWin.api.once(TEST_EVENTS.NETWORK_EVENT_UPDATED, () => { + noRequest = false; + }); + + // Click pause, load second request and make sure they don't show up. + EventUtils.sendMouseEvent({ type: "click" }, pauseButton); + await waitForPauseButtonToChange(document, true); + + await performPausedRequest(tab, monitor, toolbox); + + ok(noRequest, "There should be no activity when paused."); + assertRequestCount(store, 1); + + // Click pause again to resume monitoring. Load a third request + // and make sure they will show up. + EventUtils.sendMouseEvent({ type: "click" }, pauseButton); + await waitForPauseButtonToChange(document, false); + + await performRequestAndWait(tab, monitor, SIMPLE_URL + "?id=2"); + + ok(!noRequest, "There should be activity when resumed."); + assertRequestCount(store, 2); + + // Click pause, reload the page and check that there are + // some requests. + EventUtils.sendMouseEvent({ type: "click" }, pauseButton); + await waitForPauseButtonToChange(document, true); + + await waitForAllNetworkUpdateEvents(); + // Page reload should auto-resume + await reloadBrowser(); + await waitForPauseButtonToChange(document, false); + await performRequestAndWait(tab, monitor, SIMPLE_URL + "?id=3"); + + ok(!noRequest, "There should be activity when resumed."); + + return teardown(monitor); +}); + +/** + * Wait until a request is visible in the request list + */ +function waitForRequest(doc, url) { + return waitUntil(() => + [...doc.querySelectorAll(".request-list-item .requests-list-file")].some( + columns => columns.title.includes(url) + ) + ); +} + +/** + * Waits for the state of the paused/resume button to change. + */ +async function waitForPauseButtonToChange(doc, isPaused) { + await waitUntil( + () => + !!doc.querySelector( + `.requests-list-pause-button.devtools-${ + isPaused ? "play" : "pause" + }-icon` + ) + ); + ok( + true, + `The pause button is correctly in the ${ + isPaused ? "paused" : "resumed" + } state` + ); +} + +/** + * Asserts the number of requests in the network monitor. + */ +function assertRequestCount(store, count) { + is( + store.getState().requests.requests.length, + count, + "There should be correct number of requests" + ); +} + +/** + * Execute simple GET request and wait till it's done. + */ +async function performRequestAndWait(tab, monitor, requestURL) { + const wait = waitForRequest(monitor.panelWin.document, requestURL); + await SpecialPowers.spawn( + tab.linkedBrowser, + [requestURL], + async function (url) { + await content.wrappedJSObject.performRequests(url); + } + ); + await wait; +} + +/** + * Execute simple GET request, and uses a one time listener to + * know when the resource is available. + */ +async function performPausedRequest(tab, monitor, toolbox) { + const { onResource: waitForEventWhenPaused } = + await toolbox.resourceCommand.waitForNextResource( + toolbox.resourceCommand.TYPES.NETWORK_EVENT, + { + ignoreExistingResources: true, + } + ); + await SpecialPowers.spawn( + tab.linkedBrowser, + [SIMPLE_URL], + async function (url) { + await content.wrappedJSObject.performRequests(url); + } + ); + // Wait for NETWORK_EVENT resources to be fetched, in order to ensure + // that there is no pending request related to their processing. + await waitForEventWhenPaused; +} diff --git a/devtools/client/netmonitor/test/browser_net_persistent_logs.js b/devtools/client/netmonitor/test/browser_net_persistent_logs.js new file mode 100644 index 0000000000..ed44f8dfd1 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_persistent_logs.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the network monitor leaks on initialization and sudden destruction. + * You can also use this initialization format as a template for other tests. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SINGLE_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + Services.prefs.setBoolPref("devtools.netmonitor.persistlog", false); + + await reloadAndWait(); + + // Using waitUntil in the test is necessary to ensure all requests are added correctly. + // Because reloadAndWait call may catch early uncaught requests from initNetMonitor, so + // the actual number of requests after reloadAndWait could be wrong since all requests + // haven't finished. + await waitUntil( + () => document.querySelectorAll(".request-list-item").length === 2 + ); + is( + document.querySelectorAll(".request-list-item").length, + 2, + "The request list should have two items at this point." + ); + + await reloadAndWait(); + + await waitUntil( + () => document.querySelectorAll(".request-list-item").length === 2 + ); + // Since the reload clears the log, we still expect two requests in the log + is( + document.querySelectorAll(".request-list-item").length, + 2, + "The request list should still have two items at this point." + ); + + // Now we toggle the persistence logs on + Services.prefs.setBoolPref("devtools.netmonitor.persistlog", true); + + await reloadAndWait(); + + await waitUntil( + () => document.querySelectorAll(".request-list-item").length === 4 + ); + // Since we togged the persistence logs, we expect four items after the reload + is( + document.querySelectorAll(".request-list-item").length, + 4, + "The request list should now have four items at this point." + ); + + Services.prefs.setBoolPref("devtools.netmonitor.persistlog", false); + return teardown(monitor); + + /** + * Reload the page and wait for 2 GET requests. + */ + async function reloadAndWait() { + const wait = waitForNetworkEvents(monitor, 2); + await reloadBrowser(); + return wait; + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_post-data-json-payloads.js b/devtools/client/netmonitor/test/browser_net_post-data-json-payloads.js new file mode 100644 index 0000000000..9befb3ce49 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_post-data-json-payloads.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the POST requests display the correct information in the UI, + * for JSON payloads. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(POST_JSON_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + // Wait for header and properties view to be displayed + const wait = waitForDOM(document, "#request-panel .data-header"); + let waitForContent = waitForDOM(document, "#request-panel .properties-view"); + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "request"); + await Promise.all([wait, waitForContent]); + + const tabpanel = document.querySelector("#request-panel"); + + ok( + tabpanel.querySelector(".treeTable"), + "The request params doesn't have the indended visibility." + ); + is( + tabpanel.querySelector(".CodeMirror-code") === null, + true, + "The request post data doesn't have the indended visibility." + ); + is( + tabpanel.querySelectorAll(".raw-data-toggle") !== null, + true, + "The raw request data toggle should be displayed in this tabpanel." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + + is( + tabpanel.querySelector(".data-label").textContent, + L10N.getStr("jsonScopeName"), + "The post section doesn't have the correct title." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is(labels[0].textContent, "a", "The JSON var name was incorrect."); + is(values[0].textContent, "1", "The JSON var value was incorrect."); + + // Toggle the raw data display. This should hide the formatted display. + waitForContent = waitForDOM(document, "#request-panel .CodeMirror-code"); + const rawDataToggle = document.querySelector( + "#request-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + clickElement(rawDataToggle, monitor); + await waitForContent; + + is( + tabpanel.querySelector(".data-label").textContent, + L10N.getStr("paramsPostPayload"), + "The post section doesn't have the correct title." + ); + is( + tabpanel.querySelector(".raw-data-toggle-input .devtools-checkbox-toggle") + .checked, + true, + "The raw request toggle should be on." + ); + is( + tabpanel.querySelector(".properties-view") === null, + true, + "The formatted display should be hidden." + ); + // Bug 1514750 - Show JSON request in plain text view also + ok( + tabpanel.querySelector(".CodeMirror-code"), + "The request post data doesn't have the indended visibility." + ); + ok( + getCodeMirrorValue(monitor).includes('{"a":1}'), + "The text shown in the source editor is incorrect." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_post-data-raw-payloads-with-upload-stream-headers.js b/devtools/client/netmonitor/test/browser_net_post-data-raw-payloads-with-upload-stream-headers.js new file mode 100644 index 0000000000..403aa48906 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_post-data-raw-payloads-with-upload-stream-headers.js @@ -0,0 +1,202 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the POST requests display the correct information in the UI, + * for raw payloads with content-type headers attached to the upload stream. + */ +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(POST_RAW_WITH_HEADERS_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 3); + + const expectedRequestsContent = [ + { + headersFromUploadSectionTitle: + "Request headers from upload stream (47 B)", + uploadSectionHeaders: [ + { label: "content-type", value: "application/x-www-form-urlencoded" }, + ], + uploadSectionRawText: "content-type: application/x-www-form-urlencoded", + requestPanelFormData: [ + { label: "foo", value: '"bar"' }, + { label: "baz", value: '"123"' }, + ], + requestPanelPayload: [ + "content-type: application/x-www-form-urlencoded", + "foo=bar&baz=123", + ], + }, + { + headersFromUploadSectionTitle: + "Request headers from upload stream (47 B)", + uploadSectionHeaders: [ + { label: "content-type", value: "application/x-www-form-urlencoded" }, + ], + uploadSectionRawText: "content-type: application/x-www-form-urlencoded", + requestPanelPayload: ["content-type: application/x-www-form-urlencoded"], + }, + { + headersFromUploadSectionTitle: + "Request headers from upload stream (74 B)", + uploadSectionHeaders: [ + { label: "content-type", value: "application/x-www-form-urlencoded" }, + { label: "custom-header", value: "hello world!" }, + ], + uploadSectionRawText: + "content-type: application/x-www-form-urlencoded\r\ncustom-header: hello world!", + requestPanelFormData: [ + { label: "foo", value: '"bar"' }, + { label: "baz", value: '"123"' }, + ], + requestPanelPayload: [ + "content-type: application/x-www-form-urlencoded", + "custom-header: hello world!", + "foo=bar&baz=123", + ], + }, + ]; + + const requests = document.querySelectorAll(".request-list-item"); + store.dispatch(Actions.toggleNetworkDetails()); + + for (let i = 0; i < expectedRequestsContent.length; i++) { + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[i]); + await assertRequestContentInHeaderAndRequestSidePanels( + expectedRequestsContent[i] + ); + } + + async function assertRequestContentInHeaderAndRequestSidePanels(expected) { + // Wait for all 3 headers sections to load (Response Headers, Request Headers, Request headers from upload stream) + let wait = waitForDOM(document, "#headers-panel .accordion-item", 3); + clickOnSidebarTab(document, "headers"); + await wait; + + let tabpanel = document.querySelector("#headers-panel"); + is( + tabpanel.querySelectorAll(".accordion-item").length, + 3, + "There should be 3 header sections displayed in this tabpanel." + ); + + info("Check that the Headers in the upload stream section are correct."); + is( + tabpanel.querySelectorAll(".accordion-item .accordion-header-label")[2] + .textContent, + expected.headersFromUploadSectionTitle, + "The request headers from upload section doesn't have the correct title." + ); + + let labels = tabpanel.querySelectorAll( + ".accordion-item:last-child .accordion-content tr .treeLabelCell .treeLabel" + ); + let values = tabpanel.querySelectorAll( + ".accordion-item:last-child .accordion-content tr .treeValueCell .objectBox" + ); + + for (let i = 0; i < labels.length; i++) { + is( + labels[i].textContent, + expected.uploadSectionHeaders[i].label, + "The request header name was incorrect." + ); + is( + values[i].textContent, + expected.uploadSectionHeaders[i].value, + "The request header value was incorrect." + ); + } + + info( + "Toggle to open the raw view for the request headers from upload stream" + ); + + wait = waitForDOM( + tabpanel, + ".accordion-item:last-child .accordion-content .raw-headers-container" + ); + tabpanel.querySelector("#raw-upload-checkbox").click(); + await wait; + + const rawTextArea = tabpanel.querySelector( + ".accordion-item:last-child .accordion-content .raw-headers" + ); + is( + rawTextArea.textContent, + expected.uploadSectionRawText, + "The raw text for the request headers from upload section is correct" + ); + + info("Switch to the Request panel"); + + wait = waitForDOM(document, "#request-panel .panel-container"); + clickOnSidebarTab(document, "request"); + await wait; + + tabpanel = document.querySelector("#request-panel"); + if (expected.requestPanelFormData) { + await waitUntil( + () => + tabpanel.querySelector(".data-label").textContent == + L10N.getStr("paramsFormData") + ); + + labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + for (let i = 0; i < labels.length; i++) { + is( + labels[i].textContent, + expected.requestPanelFormData[i].label, + "The form data param name was incorrect." + ); + is( + values[i].textContent, + expected.requestPanelFormData[i].value, + "The form data param value was incorrect." + ); + } + + info("Toggle open the the request payload raw view"); + + tabpanel.querySelector("#raw-request-checkbox").click(); + } + await waitUntil( + () => + tabpanel.querySelector(".data-label").textContent == + L10N.getStr("paramsPostPayload") && + tabpanel.querySelector( + ".panel-container .editor-row-container .CodeMirror-code" + ) + ); + + // Check that the expected header lines are included in the codemirror + // text. + const actualText = tabpanel.querySelector( + ".panel-container .editor-row-container .CodeMirror-code" + ).textContent; + const requestPayloadIsCorrect = expected.requestPanelPayload.every( + content => actualText.includes(content) + ); + + is(requestPayloadIsCorrect, true, "The request payload is not correct"); + } + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_post-data-raw-payloads.js b/devtools/client/netmonitor/test/browser_net_post-data-raw-payloads.js new file mode 100644 index 0000000000..340b363bb3 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_post-data-raw-payloads.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the POST requests display the correct information in the UI, + * for raw payloads with attached content-type headers. + */ +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 1); + + // Wait for raw data toggle to be displayed + const wait = waitForDOM( + document, + "#request-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + clickOnSidebarTab(document, "request"); + await wait; + + const tabpanel = document.querySelector("#request-panel"); + + ok( + tabpanel.querySelector(".treeTable"), + "The request params doesn't have the intended visibility." + ); + Assert.strictEqual( + tabpanel.querySelector(".editor-mount"), + null, + "The request post data doesn't have the indented visibility." + ); + + is( + tabpanel.querySelectorAll(".raw-data-toggle") !== null, + true, + "The raw request data toggle should be displayed in this tabpanel." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + + is( + tabpanel.querySelector(".data-label").textContent, + L10N.getStr("paramsFormData"), + "The post section doesn't have the correct title." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + is(labels[0].textContent, "foo", "The first query param name was incorrect."); + is( + values[0].textContent, + `"bar"`, + "The first query param value was incorrect." + ); + is( + labels[1].textContent, + "baz", + "The second query param name was incorrect." + ); + is( + values[1].textContent, + `"123"`, + "The second query param value was incorrect." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_post-data.js b/devtools/client/netmonitor/test/browser_net_post-data.js new file mode 100644 index 0000000000..86533b39f8 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_post-data.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the POST requests display the correct information in the UI. + */ +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + // Set a higher panel height in order to get full CodeMirror content + Services.prefs.setIntPref("devtools.toolbox.footer.height", 600); + + const { tab, monitor } = await initNetMonitor(POST_DATA_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 2); + + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + } + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[0], + "POST", + SIMPLE_SJS + + "?foo=bar&baz=42&valueWithEqualSign=hijk=123=mnop&type=urlencoded", + { + status: 200, + statusText: "Och Aye", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12), + time: true, + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[1], + "POST", + SIMPLE_SJS + "?foo=bar&baz=42&type=multipart", + { + status: 200, + statusText: "Och Aye", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12), + time: true, + } + ); + + // Wait for raw data toggle to be displayed + const wait = waitForDOM( + document, + "#request-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + clickOnSidebarTab(document, "request"); + await wait; + await testParamsTab("urlencoded"); + + // Wait for header and CodeMirror editor to be displayed + const waitForHeader = waitForDOM(document, "#request-panel .data-header"); + const waitForSourceEditor = waitForDOM( + document, + "#request-panel .CodeMirror-code" + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[1] + ); + await Promise.all([waitForHeader, waitForSourceEditor]); + await testParamsTab("multipart"); + + return teardown(monitor); + + async function testParamsTab(type) { + const tabpanel = document.querySelector("#request-panel"); + + function checkVisibility(box) { + is( + tabpanel.querySelector(".CodeMirror-code") === null, + !box.includes("editor"), + "The request post data doesn't have the intended visibility." + ); + } + + is( + tabpanel.querySelectorAll(".raw-data-toggle").length, + type == "urlencoded" ? 1 : 0, + "The display of the raw request data toggle must be correct." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 0, + "The empty notice should not be displayed in this tabpanel." + ); + + is( + tabpanel.querySelector(".data-label").textContent, + L10N.getStr( + type == "urlencoded" ? "paramsFormData" : "paramsPostPayload" + ), + "The post section doesn't have the correct title." + ); + + const labels = tabpanel.querySelectorAll("tr .treeLabelCell .treeLabel"); + const values = tabpanel.querySelectorAll("tr .treeValueCell .objectBox"); + + if (type == "urlencoded") { + checkVisibility("request"); + is( + labels.length, + 4, + "There should be 4 param values displayed in this tabpanel." + ); + is( + labels[0].textContent, + "foo", + "The first post param name was incorrect." + ); + is( + values[0].textContent, + `"bar"`, + "The first post param value was incorrect." + ); + is( + labels[1].textContent, + "baz", + "The second post param name was incorrect." + ); + is( + values[1].textContent, + `"123"`, + "The second post param value was incorrect." + ); + is( + labels[2].textContent, + "valueWithEqualSign", + "The third post param name was incorrect." + ); + is( + values[2].textContent, + `"xyz=abc=123"`, + "The third post param value was incorrect." + ); + is( + labels[3].textContent, + "valueWithAmpersand", + "The fourth post param name was incorrect." + ); + is( + values[3].textContent, + `"abcd&1234"`, + "The fourth post param value was incorrect." + ); + } else { + checkVisibility("request editor"); + + const text = getCodeMirrorValue(monitor); + + ok( + text.includes('Content-Disposition: form-data; name="text"'), + "The text shown in the source editor is incorrect (1.1)." + ); + ok( + text.includes('Content-Disposition: form-data; name="email"'), + "The text shown in the source editor is incorrect (2.1)." + ); + ok( + text.includes('Content-Disposition: form-data; name="range"'), + "The text shown in the source editor is incorrect (3.1)." + ); + ok( + text.includes('Content-Disposition: form-data; name="Custom field"'), + "The text shown in the source editor is incorrect (4.1)." + ); + ok( + text.includes("Some text..."), + "The text shown in the source editor is incorrect (2.2)." + ); + ok( + text.includes("42"), + "The text shown in the source editor is incorrect (3.2)." + ); + ok( + text.includes("Extra data"), + "The text shown in the source editor is incorrect (4.2)." + ); + } + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js b/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js new file mode 100644 index 0000000000..aa5c3cac46 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_prefs-and-l10n.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the preferences and localization objects work correctly. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + info("Starting test... "); + + const { windowRequire } = monitor.panelWin; + const { Prefs } = windowRequire("devtools/client/netmonitor/src/utils/prefs"); + + testL10N(); + testPrefs(); + + return teardown(monitor); + + function testL10N() { + is( + typeof L10N.getStr("netmonitor.security.enabled"), + "string", + "The getStr() method didn't return a valid string." + ); + is( + typeof L10N.getFormatStr("networkMenu.totalMS2", "foo"), + "string", + "The getFormatStr() method didn't return a valid string." + ); + } + + function testPrefs() { + is( + Prefs.networkDetailsWidth, + Services.prefs.getIntPref( + "devtools.netmonitor.panes-network-details-width" + ), + "Getting a pref should work correctly." + ); + + const previousValue = Prefs.networkDetailsWidth; + const bogusValue = ~~(Math.random() * 100); + Prefs.networkDetailsWidth = bogusValue; + is( + Prefs.networkDetailsWidth, + Services.prefs.getIntPref( + "devtools.netmonitor.panes-network-details-width" + ), + "Getting a pref after it has been modified should work correctly." + ); + is( + Prefs.networkDetailsWidth, + bogusValue, + "The pref wasn't updated correctly in the preferences object." + ); + + Prefs.networkDetailsWidth = previousValue; + is( + Prefs.networkDetailsWidth, + Services.prefs.getIntPref( + "devtools.netmonitor.panes-network-details-width" + ), + "Getting a pref after it has been modified again should work correctly." + ); + is( + Prefs.networkDetailsWidth, + previousValue, + "The pref wasn't updated correctly again in the preferences object." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_prefs-reload.js b/devtools/client/netmonitor/test/browser_net_prefs-reload.js new file mode 100644 index 0000000000..523c4dc805 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_prefs-reload.js @@ -0,0 +1,327 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the prefs that should survive across tool reloads work. + */ + +add_task(async function () { + let { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + const Actions = monitor.panelWin.windowRequire( + "devtools/client/netmonitor/src/actions/index" + ); + info("Starting test... "); + + // This test reopens the network monitor a bunch of times, for different + // hosts (bottom, side, window). This seems to be slow on debug builds. + requestLongerTimeout(3); + + // Use these getters instead of caching instances inside the panel win, + // since the tool is reopened a bunch of times during this test + // and the instances will differ. + const getDoc = () => monitor.panelWin.document; + const getPrefs = () => + monitor.panelWin.windowRequire("devtools/client/netmonitor/src/utils/prefs") + .Prefs; + const getStore = () => monitor.panelWin.store; + const getState = () => getStore().getState(); + + const prefsToCheck = { + filters: { + // A custom new value to be used for the verified preference. + newValue: ["html", "css"], + // Getter used to retrieve the current value from the frontend, in order + // to verify that the pref was applied properly. + validateValue: () => + Object.entries(getState().filters.requestFilterTypes) + .filter(([type, check]) => check) + .map(([type, check]) => type), + // Predicate used to modify the frontend when setting the new pref value, + // before trying to validate the changes. + modifyFrontend: value => + value.forEach(e => + getStore().dispatch(Actions.toggleRequestFilterType(e)) + ), + }, + networkDetailsWidth: { + newValue: ~~(Math.random() * 200 + 100), + validateValue: () => + getDoc().querySelector(".monitor-panel .split-box .controlled") + .clientWidth, + modifyFrontend(value) { + getDoc().querySelector( + ".monitor-panel .split-box .controlled" + ).style.width = `${value}px`; + }, + }, + networkDetailsHeight: { + newValue: ~~(Math.random() * 300 + 100), + validateValue: () => + getDoc().querySelector(".monitor-panel .split-box .controlled") + .clientHeight, + modifyFrontend(value) { + getDoc().querySelector( + ".monitor-panel .split-box .controlled" + ).style.height = `${value}px`; + }, + }, + /* add more prefs here... */ + }; + + await testBottom(); + await testSide(); + await testWindow(); + + info("Moving toolbox back to the bottom..."); + await monitor.toolbox.switchHost("bottom"); + return teardown(monitor); + + function storeFirstPrefValues() { + info("Caching initial pref values."); + + for (const name in prefsToCheck) { + const currentValue = getPrefs()[name]; + prefsToCheck[name].firstValue = currentValue; + } + } + + function validateFirstPrefValues(isVerticalSplitter) { + info("Validating current pref values to the UI elements."); + + for (const name in prefsToCheck) { + if ( + (isVerticalSplitter && name === "networkDetailsHeight") || + (!isVerticalSplitter && name === "networkDetailsWidth") + ) { + continue; + } + + const currentValue = getPrefs()[name]; + const { firstValue, validateValue } = prefsToCheck[name]; + + is( + firstValue.toString(), + currentValue.toString(), + "Pref " + name + " should be equal to first value: " + currentValue + ); + is( + validateValue().toString(), + currentValue.toString(), + "Pref " + name + " should validate: " + currentValue + ); + } + } + + function modifyFrontend(isVerticalSplitter) { + info("Modifying UI elements to the specified new values."); + + for (const name in prefsToCheck) { + if ( + (isVerticalSplitter && name === "networkDetailsHeight") || + (!isVerticalSplitter && name === "networkDetailsWidth") + ) { + continue; + } + + const currentValue = getPrefs()[name]; + const { firstValue, newValue, validateValue } = prefsToCheck[name]; + const modFrontend = prefsToCheck[name].modifyFrontend; + + modFrontend(newValue); + info("Modified UI element affecting " + name + " to: " + newValue); + + is( + firstValue.toString(), + currentValue.toString(), + "Pref " + + name + + " should still be equal to first value: " + + currentValue + ); + isnot( + newValue.toString(), + currentValue.toString(), + "Pref " + + name + + " should't yet be equal to second value: " + + currentValue + ); + is( + validateValue().toString(), + newValue.toString(), + "The UI element affecting " + name + " should validate: " + newValue + ); + } + } + + function validateNewPrefValues(isVerticalSplitter) { + info("Invalidating old pref values to the modified UI elements."); + + for (const name in prefsToCheck) { + if ( + (isVerticalSplitter && name === "networkDetailsHeight") || + (!isVerticalSplitter && name === "networkDetailsWidth") + ) { + continue; + } + + const currentValue = getPrefs()[name]; + const { firstValue, newValue, validateValue } = prefsToCheck[name]; + + isnot( + firstValue.toString(), + currentValue.toString(), + "Pref " + name + " should't be equal to first value: " + currentValue + ); + is( + newValue.toString(), + currentValue.toString(), + "Pref " + name + " should now be equal to second value: " + currentValue + ); + is( + validateValue().toString(), + newValue.toString(), + "The UI element affecting " + name + " should validate: " + newValue + ); + } + } + + function resetFrontend(isVerticalSplitter) { + info("Resetting UI elements to the cached initial pref values."); + + for (const name in prefsToCheck) { + if ( + (isVerticalSplitter && name === "networkDetailsHeight") || + (!isVerticalSplitter && name === "networkDetailsWidth") + ) { + continue; + } + + const currentValue = getPrefs()[name]; + const { firstValue, newValue, validateValue } = prefsToCheck[name]; + const modFrontend = prefsToCheck[name].modifyFrontend; + + modFrontend(firstValue); + info("Modified UI element affecting " + name + " to: " + firstValue); + + isnot( + firstValue.toString(), + currentValue.toString(), + "Pref " + + name + + " should't yet be equal to first value: " + + currentValue + ); + is( + newValue.toString(), + currentValue.toString(), + "Pref " + + name + + " should still be equal to second value: " + + currentValue + ); + is( + validateValue().toString(), + firstValue.toString(), + "The UI element affecting " + name + " should validate: " + firstValue + ); + } + } + + async function restartNetMonitorAndSetupEnv() { + const newMonitor = await restartNetMonitor(monitor, { requestCount: 1 }); + monitor = newMonitor.monitor; + + const networkEvent = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await networkEvent; + + const wait = waitForDOM(getDoc(), ".network-details-bar"); + getStore().dispatch(Actions.toggleNetworkDetails()); + await wait; + } + + async function testBottom() { + await restartNetMonitorAndSetupEnv(); + + info("Testing prefs reload for a bottom host."); + storeFirstPrefValues(); + + // Validate and modify while toolbox is on the bottom. + validateFirstPrefValues(true); + modifyFrontend(true); + + await restartNetMonitorAndSetupEnv(); + + // Revalidate and reset frontend while toolbox is on the bottom. + validateNewPrefValues(true); + resetFrontend(true); + + await restartNetMonitorAndSetupEnv(); + + // Revalidate. + validateFirstPrefValues(true); + } + + async function testSide() { + await restartNetMonitorAndSetupEnv(); + + info("Moving toolbox to the right..."); + + await monitor.toolbox.switchHost("right"); + + // Switching hosts is not correctly waiting when DevTools run in content frame + // See Bug 1571421. + await wait(1000); + + info("Testing prefs reload for a right host."); + storeFirstPrefValues(); + + // Validate and modify frontend while toolbox is on the side. + validateFirstPrefValues(false); + modifyFrontend(false); + + await restartNetMonitorAndSetupEnv(); + + // Revalidate and reset frontend while toolbox is on the side. + validateNewPrefValues(false); + resetFrontend(false); + + await restartNetMonitorAndSetupEnv(); + + // Revalidate. + validateFirstPrefValues(false); + } + + async function testWindow() { + await restartNetMonitorAndSetupEnv(); + + info("Moving toolbox into a window..."); + + await monitor.toolbox.switchHost("window"); + + // Switching hosts is not correctly waiting when DevTools run in content frame + // See Bug 1571421. + await wait(1000); + + info("Testing prefs reload for a window host."); + storeFirstPrefValues(); + + // Validate and modify frontend while toolbox is in a window. + validateFirstPrefValues(true); + modifyFrontend(true); + + await restartNetMonitorAndSetupEnv(); + + // Revalidate and reset frontend while toolbox is in a window. + validateNewPrefValues(true); + resetFrontend(true); + + await restartNetMonitorAndSetupEnv(); + + // Revalidate. + validateFirstPrefValues(true); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_raw_headers.js b/devtools/client/netmonitor/test/browser_net_raw_headers.js new file mode 100644 index 0000000000..51c6ed7f11 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if showing raw headers works. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(POST_DATA_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 2); + + let wait = waitForDOM(document, "#headers-panel .accordion-item", 2); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + wait = waitForDOM(document, "#responseHeaders textarea.raw-headers", 1); + EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersToggle("RESPONSE")); + await wait; + + wait = waitForDOM(document, "#requestHeaders textarea.raw-headers", 1); + EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersToggle("REQUEST")); + await wait; + + testRawHeaderToggleStyle(true); + testShowRawHeaders(getSortedRequests(store.getState())[0]); + + EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersToggle("RESPONSE")); + EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersToggle("REQUEST")); + + testRawHeaderToggleStyle(false); + testHideRawHeaders(document); + + return teardown(monitor); + + /** + * Tests that checked is applied correctly + * + * @param checked + * flag indicating whether toggle is checked or not + */ + function testRawHeaderToggleStyle(checked) { + const rawHeadersRequestToggle = getRawHeadersToggle("REQUEST"); + const rawHeadersResponseToggle = getRawHeadersToggle("RESPONSE"); + + if (checked) { + is( + rawHeadersRequestToggle.checked, + true, + "The 'Raw Request Headers' toggle should be 'checked'" + ); + is( + rawHeadersResponseToggle.checked, + true, + "The 'Raw Response Headers' toggle should be 'checked'" + ); + } else { + is( + rawHeadersRequestToggle.checked, + false, + "The 'Raw Request Headers' toggle should NOT be 'checked'" + ); + is( + rawHeadersResponseToggle.checked, + false, + "The 'Raw Response Headers' toggle should NOT be 'checked'" + ); + } + } + + /* + * Tests that raw headers were displayed correctly + */ + function testShowRawHeaders(data) { + // Request headers are rendered first, so it is element with index 1 + const requestHeaders = document.querySelectorAll("textarea.raw-headers")[1] + .value; + for (const header of data.requestHeaders.headers) { + ok( + requestHeaders.includes(header.name + ": " + header.value), + "textarea contains request headers" + ); + } + // Response headers are rendered first, so it is element with index 0 + const responseHeaders = document.querySelectorAll("textarea.raw-headers")[0] + .value; + for (const header of data.responseHeaders.headers) { + ok( + responseHeaders.includes(header.name + ": " + header.value), + "textarea contains response headers" + ); + } + } + + /* + * Tests that raw headers textareas are hidden + */ + function testHideRawHeaders() { + ok( + !document.querySelector(".raw-headers-container"), + "raw request headers textarea is empty" + ); + } + + /** + * Returns the 'Raw Headers' button + */ + function getRawHeadersToggle(rawHeaderType) { + if (rawHeaderType === "RESPONSE") { + // Response header is first displayed + return document.querySelectorAll(".devtools-checkbox-toggle")[0]; + } + return document.querySelectorAll(".devtools-checkbox-toggle")[1]; + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_reload-button.js b/devtools/client/netmonitor/test/browser_net_reload-button.js new file mode 100644 index 0000000000..0111f7e4d3 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_reload-button.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the empty-requests reload button works. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + info("Starting test... "); + + const { document } = monitor.panelWin; + + const wait = waitForNetworkEvents(monitor, 1); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-reload-notice-button") + ); + await wait; + + is( + document.querySelectorAll(".request-list-item").length, + 1, + "The request list should have one item after reloading" + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js b/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js new file mode 100644 index 0000000000..49d9bbd2b2 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_req-resp-bodies.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if request and response body logging stays on after opening the console. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(JSON_LONG_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Perform first batch of requests. + await performRequests(monitor, tab, 1); + + await verifyRequest(0); + + // Switch to the webconsole. + const onWebConsole = monitor.toolbox.once("webconsole-selected"); + monitor.toolbox.selectTool("webconsole"); + await onWebConsole; + + // Switch back to the netmonitor. + const onNetMonitor = monitor.toolbox.once("netmonitor-selected"); + monitor.toolbox.selectTool("netmonitor"); + await onNetMonitor; + + // Reload debugee. + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + // Perform another batch of requests. + await performRequests(monitor, tab, 1); + + await verifyRequest(1); + + return teardown(monitor); + + async function verifyRequest(index) { + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + } + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[index], + "GET", + CONTENT_TYPE_SJS + "?fmt=json-long", + { + status: 200, + statusText: "OK", + type: "json", + fullMimeType: "text/json; charset=utf-8", + size: L10N.getFormatStr( + "networkMenu.size.kB", + L10N.numberWithDecimals(85975 / 1000, 2) + ), + time: true, + } + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_resend.js b/devtools/client/netmonitor/test/browser_net_resend.js new file mode 100644 index 0000000000..cc3c212988 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_resend.js @@ -0,0 +1,385 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if resending a request works. + */ + +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + await testResendRequest(); + } else { + await testOldEditAndResendPanel(); + } +}); + +// This tests resending a request without editing using +// the resend context menu item. This particularly covering +// the new resend functionality. +async function testResendRequest() { + const { tab, monitor } = await initNetMonitor(POST_DATA_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + await performRequests(monitor, tab, 2); + + is( + document.querySelectorAll(".request-list-item").length, + 2, + "There are currently two requests" + ); + + const firstResend = await resendRequestAndWaitForNewRequest( + monitor, + document.querySelectorAll(".request-list-item")[0] + ); + + Assert.notStrictEqual( + firstResend.originalResource.resourceId, + firstResend.newResource.resourceId, + "The resent request is different resource from the first request" + ); + + is( + firstResend.originalResource.url, + firstResend.newResource.url, + "The resent request has the same url and query parameters and the first request" + ); + + is( + firstResend.originalResource.requestHeaders.headers.length, + firstResend.newResource.requestHeaders.headers.length, + "The no of headers are the same" + ); + + firstResend.originalResource.requestHeaders.headers.forEach( + ({ name, value }) => { + const foundHeader = firstResend.newResource.requestHeaders.headers.find( + header => header.name == name + ); + is( + value, + foundHeader.value, + `The '${name}' header for the request and the resent request match` + ); + } + ); + + info("Check that the custom headers and form data are resent correctly"); + const secondResend = await resendRequestAndWaitForNewRequest( + monitor, + document.querySelectorAll(".request-list-item")[1] + ); + + Assert.notStrictEqual( + secondResend.originalResource.resourceId, + secondResend.newResource.resourceId, + "The resent request is different resource from the second request" + ); + + const customHeader = + secondResend.originalResource.requestHeaders.headers.find( + header => header.name == "custom-header-xxx" + ); + + const customHeaderInResentRequest = + secondResend.newResource.requestHeaders.headers.find( + header => header.name == "custom-header-xxx" + ); + + is( + customHeader.value, + customHeaderInResentRequest.value, + "The custom header in the resent request is the same as the second request" + ); + + is( + customHeaderInResentRequest.value, + "custom-value-xxx", + "The custom header in the resent request is correct" + ); + + is( + secondResend.originalResource.requestPostData.postData.text, + secondResend.newResource.requestPostData.postData.text, + "The form data in the resent is the same as the second request" + ); +} + +async function resendRequestAndWaitForNewRequest(monitor, originalRequestItem) { + const { document, store, windowRequire, connector } = monitor.panelWin; + const { getSelectedRequest, getDisplayedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + info("Select the request to resend"); + const expectedNoOfRequestsAfterResend = + getDisplayedRequests(store.getState()).length + 1; + + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, originalRequestItem); + await waitForHeaders; + + const originalResourceId = getSelectedRequest(store.getState()).id; + + const waitForNewRequest = waitUntil( + () => + getDisplayedRequests(store.getState()).length == + expectedNoOfRequestsAfterResend && + getSelectedRequest(store.getState()).id !== originalResourceId + ); + + info("Open the context menu and select the resend for the request"); + EventUtils.sendMouseEvent({ type: "contextmenu" }, originalRequestItem); + await selectContextMenuItem(monitor, "request-list-context-resend-only"); + await waitForNewRequest; + + const newResourceId = getSelectedRequest(store.getState()).id; + + // Make sure we fetch the request headers and post data for the + // new request so we can assert them. + await connector.requestData(newResourceId, "requestHeaders"); + await connector.requestData(newResourceId, "requestPostData"); + + return { + originalResource: getRequestById(store.getState(), originalResourceId), + newResource: getRequestById(store.getState(), newResourceId), + }; +} + +// This is a basic test for the old edit and resend panel +// This should be removed soon in Bug 1745416 when we remove +// the old panel functionality. +async function testOldEditAndResendPanel() { + const ADD_QUERY = "t1=t2"; + const ADD_HEADER = "Test-header: true"; + const ADD_UA_HEADER = "User-Agent: Custom-Agent"; + const ADD_POSTDATA = "&t3=t4"; + + const { tab, monitor } = await initNetMonitor(POST_DATA_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSelectedRequest, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 2); + + const origItemId = getSortedRequests(store.getState())[0].id; + + store.dispatch(Actions.selectRequest(origItemId)); + await waitForRequestData( + store, + ["requestHeaders", "requestPostData"], + origItemId + ); + + let origItem = getSortedRequests(store.getState())[0]; + + // add a new custom request cloned from selected request + + store.dispatch(Actions.cloneSelectedRequest()); + await testCustomForm(origItem); + + let customItem = getSelectedRequest(store.getState()); + testCustomItem(customItem, origItem); + + // edit the custom request + await editCustomForm(); + + // FIXME: reread the customItem, it's been replaced by a new object (immutable!) + customItem = getSelectedRequest(store.getState()); + testCustomItemChanged(customItem, origItem); + + // send the new request + const wait = waitForNetworkEvents(monitor, 1); + store.dispatch(Actions.sendCustomRequest()); + await wait; + + let sentItem; + // Testing sent request will require updated requestHeaders and requestPostData, + // we must wait for both properties get updated before starting test. + await waitUntil(() => { + sentItem = getSelectedRequest(store.getState()); + origItem = getSortedRequests(store.getState())[0]; + return ( + sentItem && + sentItem.requestHeaders && + sentItem.requestPostData && + origItem && + origItem.requestHeaders && + origItem.requestPostData + ); + }); + + await testSentRequest(sentItem, origItem); + + // Ensure the UI shows the new request, selected, and that the detail panel was closed. + is( + getSortedRequests(store.getState()).length, + 3, + "There are 3 requests shown" + ); + is( + document + .querySelector(".request-list-item.selected") + .getAttribute("data-id"), + sentItem.id, + "The sent request is selected" + ); + is( + document.querySelector(".network-details-bar"), + null, + "The detail panel is hidden" + ); + + await teardown(monitor); + + function testCustomItem(item, orig) { + is( + item.method, + orig.method, + "item is showing the same method as original request" + ); + is(item.url, orig.url, "item is showing the same URL as original request"); + } + + function testCustomItemChanged(item, orig) { + const { url } = item; + const expectedUrl = orig.url + "&" + ADD_QUERY; + + is(url, expectedUrl, "menu item is updated to reflect url entered in form"); + } + + /* + * Test that the New Request form was populated correctly + */ + async function testCustomForm(data) { + await waitUntil(() => document.querySelector(".custom-request-panel")); + is( + document.getElementById("custom-method-value").value, + data.method, + "new request form showing correct method" + ); + + is( + document.getElementById("custom-url-value").value, + data.url, + "new request form showing correct url" + ); + + const query = document.getElementById("custom-query-value"); + is( + query.value, + "foo=bar\nbaz=42\ntype=urlencoded", + "new request form showing correct query string" + ); + + const headers = document + .getElementById("custom-headers-value") + .value.split("\n"); + for (const { name, value } of data.requestHeaders.headers) { + ok( + headers.includes(name + ": " + value), + "form contains header from request" + ); + } + + const postData = document.getElementById("custom-postdata-value"); + is( + postData.value, + data.requestPostData.postData.text, + "new request form showing correct post data" + ); + } + + /* + * Add some params and headers to the request form + */ + async function editCustomForm() { + monitor.panelWin.focus(); + + const query = document.getElementById("custom-query-value"); + const queryFocus = once(query, "focus", false); + // Bug 1195825: Due to some unexplained dark-matter with promise, + // focus only works if delayed by one tick. + query.setSelectionRange(query.value.length, query.value.length); + executeSoon(() => query.focus()); + await queryFocus; + + // add params to url query string field + typeInNetmonitor(["VK_RETURN"], monitor); + typeInNetmonitor(ADD_QUERY, monitor); + + const headers = document.getElementById("custom-headers-value"); + const headersFocus = once(headers, "focus", false); + headers.setSelectionRange(headers.value.length, headers.value.length); + headers.focus(); + await headersFocus; + + // add a header + typeInNetmonitor(["VK_RETURN"], monitor); + typeInNetmonitor(ADD_HEADER, monitor); + + // add a User-Agent header, to check if default headers can be modified + // (there will be two of them, first gets overwritten by the second) + typeInNetmonitor(["VK_RETURN"], monitor); + typeInNetmonitor(ADD_UA_HEADER, monitor); + + const postData = document.getElementById("custom-postdata-value"); + const postFocus = once(postData, "focus", false); + postData.setSelectionRange(postData.value.length, postData.value.length); + postData.focus(); + await postFocus; + + // add to POST data once textarea has updated + await waitUntil(() => postData.textContent !== ""); + typeInNetmonitor(ADD_POSTDATA, monitor); + } + + /* + * Make sure newly created event matches expected request + */ + async function testSentRequest(data, origData) { + is(data.method, origData.method, "correct method in sent request"); + is(data.url, origData.url + "&" + ADD_QUERY, "correct url in sent request"); + + const { headers } = data.requestHeaders; + const hasHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_HEADER); + ok(hasHeader, "new header added to sent request"); + + const hasUAHeader = headers.some( + h => `${h.name}: ${h.value}` == ADD_UA_HEADER + ); + ok(hasUAHeader, "User-Agent header added to sent request"); + + is( + data.requestPostData.postData.text, + origData.requestPostData.postData.text + ADD_POSTDATA, + "post data added to sent request" + ); + } +} diff --git a/devtools/client/netmonitor/test/browser_net_resend_cors.js b/devtools/client/netmonitor/test/browser_net_resend_cors.js new file mode 100644 index 0000000000..f9243ce6d5 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if resending a CORS request avoids the security checks and doesn't send + * a preflight OPTIONS request (bug 1270096 and friends) + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CORS_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getRequestById, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const requestUrl = "https://test1.example.com" + CORS_SJS_PATH; + + info("Waiting for OPTIONS, then POST"); + const onEvents = waitForNetworkEvents(monitor, 2); + await SpecialPowers.spawn( + tab.linkedBrowser, + [requestUrl], + async function (url) { + content.wrappedJSObject.performRequests( + url, + "triggering/preflight", + "post-data" + ); + } + ); + await onEvents; + + // Check the requests that were sent + let sortedRequests = getSortedRequests(store.getState()); + + const optRequest = sortedRequests[0]; + is(optRequest.method, "OPTIONS", `The OPTIONS request has the right method`); + is(optRequest.url, requestUrl, `The OPTIONS request has the right URL`); + + const postRequest = sortedRequests[1]; + is(postRequest.method, "POST", `The POST request has the right method`); + is(postRequest.url, requestUrl, `The POST request has the right URL`); + + // Resend both requests without modification. Wait for resent OPTIONS, then POST. + // POST is supposed to have no preflight OPTIONS request this time (CORS is disabled) + for (let item of [optRequest, postRequest]) { + const onRequest = waitForNetworkEvents(monitor, 1); + info(`Selecting the ${item.method} request`); + store.dispatch(Actions.selectRequest(item.id)); + + // Wait for requestHeaders and responseHeaders are required when fetching data + // from back-end. + await waitUntil(() => { + item = getRequestById(store.getState(), item.id); + return item.requestHeaders && item.responseHeaders; + }); + + info(`Cloning the ${item.method} request into a custom clone`); + store.dispatch(Actions.cloneRequest(item.id)); + + info("Sending the cloned request (without change)"); + store.dispatch(Actions.sendCustomRequest(item.id)); + + info("Waiting for the resent request"); + await onRequest; + } + + // Retrieve the new list of sorted requests, which should include 2 resent + // requests. + sortedRequests = getSortedRequests(store.getState()); + is(sortedRequests.length, 4, "There are 4 requests in total"); + + const resentOptRequest = sortedRequests[2]; + is( + resentOptRequest.method, + "OPTIONS", + `The resent OPTIONS request has the right method` + ); + is( + resentOptRequest.url, + requestUrl, + `The resent OPTIONS request has the right URL` + ); + is( + resentOptRequest.status, + "200", + `The resent OPTIONS response has the right status` + ); + is( + resentOptRequest.blockedReason, + 0, + `The resent OPTIONS request was not blocked` + ); + + let resentPostRequest = sortedRequests[3]; + is( + resentPostRequest.method, + "POST", + `The resent POST request has the right method` + ); + is( + resentPostRequest.url, + requestUrl, + `The resent POST request has the right URL` + ); + is( + resentPostRequest.status, + "200", + `The resent POST response has the right status` + ); + is( + resentPostRequest.blockedReason, + 0, + `The resent POST request was not blocked` + ); + + await Promise.all([ + connector.requestData(resentPostRequest.id, "requestPostData"), + connector.requestData(resentPostRequest.id, "responseContent"), + ]); + + // Wait until responseContent and requestPostData are available. + await waitUntil(() => { + resentPostRequest = getRequestById(store.getState(), resentPostRequest.id); + return ( + resentPostRequest.responseContent && resentPostRequest.requestPostData + ); + }); + + is( + resentPostRequest.requestPostData.postData.text, + "post-data", + "The resent POST request has the right POST data" + ); + is( + resentPostRequest.responseContent.content.text, + "Access-Control-Allow-Origin: *", + "The resent POST response has the right content" + ); + + info("Finishing the test"); + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_resend_csp.js b/devtools/client/netmonitor/test/browser_net_resend_csp.js new file mode 100644 index 0000000000..bb087470d6 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_resend_csp.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if resending an image request uses the same content type + * and hence is not blocked by the CSP of the page. + */ + +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + ok( + true, + "Skip this test when pref is true, because this panel won't be default when that is the case." + ); + return; + } + const { tab, monitor } = await initNetMonitor(CSP_RESEND_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Executes 1 request + await performRequests(monitor, tab, 1); + + // Select the image request + const imgRequest = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, imgRequest); + + // Stores original request for comparison of values later + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const origReq = getSelectedRequest(store.getState()); + + // Context Menu > "Resend" + EventUtils.sendMouseEvent({ type: "contextmenu" }, imgRequest); + + const waitForResentRequest = waitForNetworkEvents(monitor, 1); + await selectContextMenuItem(monitor, "request-list-context-resend-only"); + await waitForResentRequest; + + // Selects request that was resent + const selReq = getSelectedRequest(store.getState()); + + // Finally, some sanity checks + ok(selReq.url.endsWith("test-image.png"), "Correct request selected"); + Assert.strictEqual(origReq.url, selReq.url, "Orig and Sel url match"); + + Assert.strictEqual(selReq.cause.type, "img", "Correct type of selected"); + Assert.strictEqual( + origReq.cause.type, + selReq.cause.type, + "Orig and Sel type match" + ); + + const cspOBJ = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + return JSON.parse(content.document.cspJSON); + }); + + const policies = cspOBJ["csp-policies"]; + is(policies.length, 1, "CSP: should be one policy"); + const policy = policies[0]; + is(`${policy["img-src"]}`, "*", "CSP: img-src should be *"); + + await teardown(monitor); +}); + +/** + * Tests if resending an image request uses the same content type + * and hence is not blocked by the CSP of the page. + */ + +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + const { tab, monitor } = await initNetMonitor(CSP_RESEND_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire( + "devtools/client/netmonitor/src/actions/index" + ); + store.dispatch(Actions.batchEnable(false)); + + // Executes 1 request + await performRequests(monitor, tab, 1); + + // Select the image request + const imgRequest = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, imgRequest); + + // Stores original request for comparison of values later + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const origReq = getSelectedRequest(store.getState()); + + // Context Menu > "Resend" + EventUtils.sendMouseEvent({ type: "contextmenu" }, imgRequest); + + info("Opening the new request panel"); + const waitForPanels = waitUntil( + () => + document.querySelector(".http-custom-request-panel") && + document.querySelector("#http-custom-request-send-button").disabled === + false + ); + + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + await waitForPanels; + + const waitForResentRequest = waitForNetworkEvents(monitor, 1); + const buttonSend = document.querySelector( + "#http-custom-request-send-button" + ); + buttonSend.click(); + await waitForResentRequest; + + // Selects request that was resent + const selReq = getSelectedRequest(store.getState()); + + // Finally, some sanity checks + ok(selReq.url.endsWith("test-image.png"), "Correct request selected"); + Assert.strictEqual(origReq.url, selReq.url, "Orig and Sel url match"); + + Assert.strictEqual(selReq.cause.type, "img", "Correct type of selected"); + Assert.strictEqual( + origReq.cause.type, + selReq.cause.type, + "Orig and Sel type match" + ); + + const cspOBJ = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async () => { + return JSON.parse(content.document.cspJSON); + } + ); + + const policies = cspOBJ["csp-policies"]; + is(policies.length, 1, "CSP: should be one policy"); + const policy = policies[0]; + is(`${policy["img-src"]}`, "*", "CSP: img-src should be *"); + + await teardown(monitor); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_resend_headers.js b/devtools/client/netmonitor/test/browser_net_resend_headers.js new file mode 100644 index 0000000000..3096bcb727 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_resend_headers.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if custom request headers are not ignored (bug 1270096 and friends) + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_SJS, { + requestCount: 1, + }); + info("Starting test... "); + + const { store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { requestData } = connector; + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const requestUrl = HTTPS_SIMPLE_SJS; + const requestHeaders = [ + { name: "Host", value: "fakehost.example.com" }, + { name: "User-Agent", value: "Testzilla" }, + { name: "Referer", value: "http://example.com/referrer" }, + { name: "Accept", value: "application/jarda" }, + { name: "Accept-Encoding", value: "compress, identity, funcoding" }, + { name: "Accept-Language", value: "cs-CZ" }, + ]; + + const wait = waitForNetworkEvents(monitor, 1); + connector.networkCommand.sendHTTPRequest({ + url: requestUrl, + method: "POST", + headers: requestHeaders, + body: "Hello", + cause: { + loadingDocumentUri: "http://example.com", + stacktraceAvailable: true, + type: "xhr", + }, + }); + await wait; + + let item = getSortedRequests(store.getState())[0]; + + ok(item.requestHeadersAvailable, "headers are available for lazily fetching"); + + if (item.requestHeadersAvailable && !item.requestHeaders) { + requestData(item.id, "requestHeaders"); + } + + // Wait until requestHeaders packet gets updated. + await waitForRequestData(store, ["requestHeaders"]); + + item = getSortedRequests(store.getState())[0]; + is(item.method, "POST", "The request has the right method"); + is(item.url, requestUrl, "The request has the right URL"); + + for (const { name, value } of item.requestHeaders.headers) { + info(`Request header: ${name}: ${value}`); + } + + function hasRequestHeader(name, value) { + const { headers } = item.requestHeaders; + return headers.some(h => h.name === name && h.value === value); + } + + function hasNotRequestHeader(name) { + const { headers } = item.requestHeaders; + return headers.every(h => h.name !== name); + } + + for (const { name, value } of requestHeaders) { + ok(hasRequestHeader(name, value), `The ${name} header has the right value`); + } + + // Check that the Cookie header was not added silently (i.e., that the request is + // anonymous. + for (const name of ["Cookie"]) { + ok(hasNotRequestHeader(name), `The ${name} header is not present`); + } + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_resend_hidden_headers.js b/devtools/client/netmonitor/test/browser_net_resend_hidden_headers.js new file mode 100644 index 0000000000..ea5ca7b36c --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_resend_hidden_headers.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that custom request headers are sent even without clicking on the original request (bug 1583397) + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const requestUrl = HTTPS_SIMPLE_SJS; + const requestHeaders = [ + { name: "Accept", value: "application/vnd.example+json" }, + ]; + + const originalRequest = waitForNetworkEvents(monitor, 1); + connector.networkCommand.sendHTTPRequest({ + url: requestUrl, + method: "GET", + headers: requestHeaders, + cause: { + loadingDocumentUri: "http://example.com", + stacktraceAvailable: true, + type: "xhr", + }, + }); + await originalRequest; + + info("Sent original request"); + + const originalItem = getSortedRequests(store.getState())[0]; + + store.dispatch(Actions.cloneRequest(originalItem.id)); + + const clonedRequest = waitForNetworkEvents(monitor, 1); + + store.dispatch(Actions.sendCustomRequest(originalItem.id)); + + await clonedRequest; + + info("Resent request"); + + let clonedItem = getSortedRequests(store.getState())[1]; + + await waitForRequestData(store, ["requestHeaders"], clonedItem.id); + + clonedItem = getSortedRequests(store.getState())[1]; + + for (const { name, value } of clonedItem.requestHeaders.headers) { + info(`Request header: ${name}: ${value}`); + } + + function hasRequestHeader(name, value) { + const { headers } = clonedItem.requestHeaders; + return headers.some(h => h.name === name && h.value === value); + } + + function hasNotRequestHeader(name) { + const { headers } = clonedItem.requestHeaders; + return headers.every(h => h.name !== name); + } + + for (const { name, value } of requestHeaders) { + ok(hasRequestHeader(name, value), `The ${name} header has the right value`); + } + + // Check that the Cookie header was not added silently (i.e., that the request is + // anonymous. + for (const name of ["Cookie"]) { + ok(hasNotRequestHeader(name), `The ${name} header is not present`); + } + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_resend_xhr.js b/devtools/client/netmonitor/test/browser_net_resend_xhr.js new file mode 100644 index 0000000000..a32027663c --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_resend_xhr.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if resending a request works. + */ + +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + ok( + true, + "Skip this test when pref is true, because this panel won't be default when that is the case." + ); + return; + } + const { tab, monitor } = await initNetMonitor(POST_RAW_URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Executes 1 request + await performRequests(monitor, tab, 1); + + // Selects 1st request + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + + // Stores original request for comparison of values later + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const originalRequest = getSelectedRequest(store.getState()); + + const waitForResentRequestEvent = waitForNetworkEvents(monitor, 1); + // Context Menu > "Resend" + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest); + await selectContextMenuItem(monitor, "request-list-context-resend-only"); + await waitForResentRequestEvent; + + // Selects request that was resent + const selectedRequest = getSelectedRequest(store.getState()); + + // Compares if the requests are the same. + Assert.strictEqual( + originalRequest.url, + selectedRequest.url, + "Both requests are the same" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_response_CORS_blocked.js b/devtools/client/netmonitor/test/browser_net_response_CORS_blocked.js new file mode 100644 index 0000000000..d3dd72e6f7 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_response_CORS_blocked.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that requests blocked by CORS have a notification in the response panel + * and that it is not present when requests are not blocked + */ + +add_task(async function testCORSNotificationPresent() { + info("Test that CORS notification is present"); + + const { tab, monitor } = await initNetMonitor(HTTPS_CORS_URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + + info("making request to a origin that doesn't allow cross origin"); + const requestUrl = HTTPS_EXAMPLE_ORG_URL + "sjs_simple-test-server.sjs"; + await SpecialPowers.spawn( + tab.linkedBrowser, + [requestUrl], + async function (url) { + content.wrappedJSObject.performRequests( + url, + "triggering/preflight", + "post-data" + ); + } + ); + + info("Waiting until the requests appear in netmonitor"); + await wait; + + info("selecting first request"); + const firstItem = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstItem); + + const waitForRespPanel = waitForDOM( + document, + "#response-panel .notification" + ); + + info("switching to response panel"); + const respPanelButton = document.querySelector("#response-tab"); + EventUtils.sendMouseEvent({ type: "click" }, respPanelButton); + await waitForRespPanel; + + info("selecting CORS notification"); + const CORSNotification = document.querySelector( + '#response-panel .notification[data-key="CORS-error"] .messageText' + ); + ok(CORSNotification, "CORS Notification Present"); + is( + CORSNotification?.innerText, + "Response body is not available to scripts (Reason: CORS Missing Allow Origin)", + "Notification text is correct" + ); + + await teardown(monitor); +}); + +add_task(async function testCORSNotificationNotPresent() { + info("Test that CORS notification is not present"); + const { tab, monitor } = await initNetMonitor(CORS_URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + + info("Making request to a origin that allows cross origin"); + const requestUrl = "https://test1.example.com" + CORS_SJS_PATH; + await SpecialPowers.spawn( + tab.linkedBrowser, + [requestUrl], + async function (url) { + content.wrappedJSObject.performRequests( + url, + "triggering/preflight", + "post-data" + ); + } + ); + info("waiting for requests to appear in netmonitor"); + await wait; + + info("selecting first request"); + const firstItem = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstItem); + + const waitForRespPanel = waitForDOM(document, "#response-panel"); + + info("switching to response panel"); + const respPanelButton = document.querySelector("#response-tab"); + EventUtils.sendMouseEvent({ type: "click" }, respPanelButton); + await waitForRespPanel; + + info("try to select CORS notification"); + const CORSNotification = document.querySelector( + '#response-panel .notification[data-key="CORS-error"] .messageText' + ); + ok(!CORSNotification, "CORS notification not present"); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_response_node-expanded.js b/devtools/client/netmonitor/test/browser_net_response_node-expanded.js new file mode 100644 index 0000000000..734b84d3bc --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_response_node-expanded.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the node that was expanded is still expanded when we are filtering + * in the Response Panel. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(JSON_LONG_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + await performRequests(monitor, tab, 1); + + info("selecting first request"); + const firstRequestItem = document.querySelectorAll(".request-list-item")[0]; + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequestItem); + + info("switching to response panel"); + const waitForRespPanel = waitForDOM( + document, + "#response-panel .properties-view" + ); + const respPanelButton = document.querySelector("#response-tab"); + respPanelButton.click(); + await waitForRespPanel; + + const firstRow = document.querySelector( + "#response-panel tr.treeRow.objectRow" + ); + const waitOpenNode = waitForDOM(document, "tr#\\/0\\/greeting"); + const toggleButton = firstRow.querySelector("td span.treeIcon"); + + toggleButton.click(); + await waitOpenNode; + + is(firstRow.classList.contains("opened"), true, "the node is open"); + + document.querySelector("#response-panel .devtools-filterinput").focus(); + EventUtils.sendString("greeting"); + + // Wait till there are 2048 resources rendered in the results. + await waitForDOMIfNeeded(document, "#response-panel tr.treeRow", 2048); + + is(firstRow.classList.contains("opened"), true, "the node remains open"); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_save_response_as.js b/devtools/client/netmonitor/test/browser_net_save_response_as.js new file mode 100644 index 0000000000..e8e3918ecb --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_save_response_as.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +/** + * Tests if saving a response to a file works.. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + CONTENT_TYPE_WITHOUT_CACHE_URL, + { requestCount: 1 } + ); + info("Starting test... "); + + const { document } = monitor.panelWin; + + // Execute requests. + await performRequests(monitor, tab, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS); + + // Create the folder the gzip file will be saved into + const destDir = createTemporarySaveDirectory(); + let destFile; + + MockFilePicker.displayDirectory = destDir; + const saveDialogClosedPromise = new Promise(resolve => { + MockFilePicker.showCallback = function (fp) { + info("MockFilePicker showCallback"); + const fileName = fp.defaultString; + destFile = destDir.clone(); + destFile.append(fileName); + MockFilePicker.setFiles([destFile]); + + resolve(destFile.path); + }; + }); + + registerCleanupFunction(function () { + MockFilePicker.cleanup(); + destDir.remove(true); + }); + + // Select gzip request. + + info("Open the context menu"); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[6] + ); + + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[6] + ); + + info("Open the save dialog"); + await selectContextMenuItem(monitor, "request-list-context-save-response-as"); + + info("Wait for the save dialog to close"); + const savedPath = await saveDialogClosedPromise; + + const expectedFile = destDir.clone(); + expectedFile.append("sjs_content-type-test-server.sjs"); + + is(savedPath, expectedFile.path, "Response was saved to correct path"); + + info("Wait for the downloaded file to be fully saved to disk: " + savedPath); + await TestUtils.waitForCondition(async () => { + if (!(await IOUtils.exists(savedPath))) { + return false; + } + const { size } = await IOUtils.stat(savedPath); + return size > 0; + }); + + const buffer = await IOUtils.read(savedPath); + const savedFileContent = new TextDecoder().decode(buffer); + + // The content is set by https://searchfox.org/mozilla-central/source/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs#360 + // (the "gzip" case) + is( + savedFileContent, + new Array(1000).join("Hello gzip!"), + "Saved response has the correct text" + ); + + await teardown(monitor); +}); + +function createTemporarySaveDirectory() { + const saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + return saveDir; +} diff --git a/devtools/client/netmonitor/test/browser_net_search-results.js b/devtools/client/netmonitor/test/browser_net_search-results.js new file mode 100644 index 0000000000..b3fd21ec98 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_search-results.js @@ -0,0 +1,233 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test search match functionality. + * Search panel is visible and clicking matches shows them in the request details. + */ + +add_task(async function () { + await pushPref("devtools.netmonitor.features.search", true); + + const { tab, monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + + // Action should be processed synchronously in tests. + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const SEARCH_STRING = "test"; + // Execute two XHRs and wait until they are finished. + const URLS = [ + HTTPS_SEARCH_SJS + "?value=test1", + HTTPS_SEARCH_SJS + "?value=test2", + ]; + + const wait = waitForNetworkEvents(monitor, 2); + await SpecialPowers.spawn(tab.linkedBrowser, [URLS], makeRequests); + await wait; + + // Open the Search panel + await store.dispatch(Actions.openSearch()); + + // Fill Filter input with text and check displayed messages. + // The filter should be focused automatically. + typeInNetmonitor(SEARCH_STRING, monitor); + EventUtils.synthesizeKey("KEY_Enter"); + + // Wait until there are two resources rendered in the results + await waitForDOMIfNeeded( + document, + ".search-panel-content .treeRow.resourceRow", + 2 + ); + + const searchMatchContents = document.querySelectorAll( + ".search-panel-content .treeRow .treeIcon" + ); + + for (let i = searchMatchContents.length - 1; i >= 0; i--) { + clickElement(searchMatchContents[i], monitor); + } + + // Wait until there are two resources rendered in the results + await waitForDOMIfNeeded( + document, + ".search-panel-content .treeRow.resultRow", + 12 + ); + + // Check the matches + const matches = document.querySelectorAll( + ".search-panel-content .treeRow.resultRow" + ); + + await checkSearchResult( + monitor, + matches[0], + "#headers-panel", + ".url-preview .properties-view", + ".treeRow", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[1], + "#headers-panel", + "#responseHeaders .properties-view", + ".treeRow.selected", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[2], + "#headers-panel", + "#requestHeaders .properties-view", + ".treeRow.selected", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[3], + "#cookies-panel", + "#responseCookies .properties-view", + ".treeRow.selected", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[4], + "#response-panel", + ".CodeMirror-code", + ".CodeMirror-activeline", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[5], + "#headers-panel", + ".url-preview .properties-view", + ".treeRow", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[6], + "#headers-panel", + "#responseHeaders .properties-view", + ".treeRow.selected", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[7], + "#headers-panel", + "#requestHeaders .properties-view", + ".treeRow.selected", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[8], + "#headers-panel", + "#requestHeaders .properties-view", + ".treeRow.selected", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[9], + "#cookies-panel", + "#responseCookies .properties-view", + ".treeRow.selected", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[10], + "#cookies-panel", + "#requestCookies .properties-view", + ".treeRow.selected", + [SEARCH_STRING] + ); + await checkSearchResult( + monitor, + matches[11], + "#response-panel", + ".CodeMirror-code", + ".CodeMirror-activeline", + [SEARCH_STRING] + ); + + await teardown(monitor); +}); + +async function makeRequests(urls) { + await content.wrappedJSObject.get(urls[0]); + await content.wrappedJSObject.get(urls[1]); + info("XHR Requests executed"); +} + +/** + * Check whether the search result is correctly linked with the related information + */ +async function checkSearchResult( + monitor, + match, + panelSelector, + panelContentSelector, + panelDetailSelector, + expected +) { + const { document } = monitor.panelWin; + + // Scroll the match into view so that it's clickable + match.scrollIntoView(); + + // Click on the match to show it + clickElement(match, monitor); + + console.log(`${panelSelector} ${panelContentSelector}`); + await waitFor(() => + document.querySelector(`${panelSelector} ${panelContentSelector}`) + ); + + const tabpanel = document.querySelector(panelSelector); + const content = tabpanel.querySelectorAll( + `${panelContentSelector} ${panelDetailSelector}` + ); + + is( + content.length, + expected.length, + `There should be ${expected.length} item${ + expected.length === 1 ? "" : "s" + } displayed in this tabpanel` + ); + + // Make sure only 1 item is selected + if (panelDetailSelector === ".treeRow.selected") { + const selectedElements = tabpanel.querySelectorAll(panelDetailSelector); + is( + selectedElements.length, + 1, + `There should be only 1 item selected, found ${selectedElements.length} items selected` + ); + } + + if (content.length === expected.length) { + for (let i = 0; i < expected.length; i++) { + is( + content[i].textContent.includes(expected[i]), + true, + `Content must include ${expected[i]}` + ); + } + } +} diff --git a/devtools/client/netmonitor/test/browser_net_security-details.js b/devtools/client/netmonitor/test/browser_net_security-details.js new file mode 100644 index 0000000000..c986c4be9f --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_security-details.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that Security details tab contains the expected data. + */ + +add_task(async function () { + await pushPref("security.pki.certificate_transparency.mode", 1); + + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Performing a secure request."); + const REQUESTS_URL = "https://example.com" + CORS_SJS_PATH; + const wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [REQUESTS_URL], + async function (url) { + content.wrappedJSObject.performRequests(1, url); + } + ); + await wait; + + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "security"); + await waitUntil(() => + document.querySelector("#security-panel .security-info-value") + ); + + const tabpanel = document.querySelector("#security-panel"); + const textboxes = tabpanel.querySelectorAll(".security-info-value"); + + // Connection + // The protocol will be TLS but the exact version depends on which protocol + // the test server example.com supports. + const protocol = textboxes[0].textContent; + ok(protocol.startsWith('"TLS'), "The protocol " + protocol + " seems valid."); + + // The cipher suite used by the test server example.com might change at any + // moment but all of them should start with "TLS_". + // http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml + const suite = textboxes[1].textContent; + ok(suite.startsWith('"TLS_'), "The suite " + suite + " seems valid."); + + // Host + is( + tabpanel.querySelectorAll(".treeLabel.objectLabel")[1].textContent, + "Host example.com:", + "Label has the expected value." + ); + // These two values can change. So only check they're not empty. + Assert.notStrictEqual( + textboxes[2].textContent, + "", + "Label value is not empty." + ); + Assert.notStrictEqual( + textboxes[3].textContent, + "", + "Label value is not empty." + ); + is(textboxes[4].textContent, '"Disabled"', "Label has the expected value."); + is(textboxes[5].textContent, '"Disabled"', "Label has the expected value."); + + // Cert + is( + textboxes[6].textContent, + '"example.com"', + "Label has the expected value." + ); + is( + textboxes[7].textContent, + '"<Not Available>"', + "Label has the expected value." + ); + is( + textboxes[8].textContent, + '"<Not Available>"', + "Label has the expected value." + ); + + is( + textboxes[9].textContent, + '"Temporary Certificate Authority"', + "Label has the expected value." + ); + is( + textboxes[10].textContent, + '"Mozilla Testing"', + "Label has the expected value." + ); + is( + textboxes[11].textContent, + '"Profile Guided Optimization"', + "Label has the expected value." + ); + + // Locale sensitive and varies between timezones. Can't compare equality or + // the test fails depending on which part of the world the test is executed. + + // cert validity begins + isnot(textboxes[12].textContent, "", "Label was not empty."); + // cert validity expires + isnot(textboxes[13].textContent, "", "Label was not empty."); + + // cert sha1 fingerprint + isnot(textboxes[14].textContent, "", "Label was not empty."); + // cert sha256 fingerprint + isnot(textboxes[15].textContent, "", "Label was not empty."); + + // Certificate transparency + isnot(textboxes[16].textContent, "", "Label was not empty."); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_security-error.js b/devtools/client/netmonitor/test/browser_net_security-error.js new file mode 100644 index 0000000000..19959d7671 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_security-error.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that Security details tab shows an error message with broken connections. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Requesting a resource that has a certificate problem."); + + const requestsDone = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.wrappedJSObject.performRequests(1, "https://nocert.example.com"); + }); + await requestsDone; + + const securityInfoLoaded = waitForDOM(document, ".security-info-value"); + store.dispatch(Actions.toggleNetworkDetails()); + + await waitUntil(() => document.querySelector("#security-tab")); + clickOnSidebarTab(document, "security"); + await securityInfoLoaded; + + const errormsg = document.querySelector(".security-info-value"); + isnot(errormsg.textContent, "", "Error message is not empty."); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_security-icon-click.js b/devtools/client/netmonitor/test/browser_net_security-icon-click.js new file mode 100644 index 0000000000..a51638e68d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_security-icon-click.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that clicking on the security indicator opens the security details tab. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Requesting a resource over HTTPS."); + await performRequestAndWait( + "https://example.com" + CORS_SJS_PATH + "?request_2" + ); + await performRequestAndWait( + "https://example.com" + CORS_SJS_PATH + "?request_1" + ); + + is(store.getState().requests.requests.length, 2, "Two events event logged."); + + await clickAndTestSecurityIcon(); + + info("Selecting headers panel again."); + clickOnSidebarTab(document, "headers"); + + info("Sorting the items by filename."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-file-button") + ); + + info( + "Testing that security icon can be clicked after the items were sorted." + ); + + await clickAndTestSecurityIcon(); + + return teardown(monitor); + + async function performRequestAndWait(url) { + const wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ url }], + async function (args) { + content.wrappedJSObject.performRequests(1, args.url); + } + ); + return wait; + } + + async function clickAndTestSecurityIcon() { + const icon = document.querySelector(".requests-security-state-icon"); + info( + "Clicking security icon of the first request and waiting for panel update." + ); + EventUtils.synthesizeMouseAtCenter(icon, {}, monitor.panelWin); + await waitUntil(() => + document.querySelector("#security-panel .security-info-value") + ); + + ok( + document.querySelector("#security-tab[aria-selected=true]"), + "Security tab is selected." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_security-redirect.js b/devtools/client/netmonitor/test/browser_net_security-redirect.js new file mode 100644 index 0000000000..3ab7211fe4 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_security-redirect.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test a http -> https redirect shows secure icon only for redirected https + * request. + */ + +add_task(async function () { + // This test explicitly asserts http -> https redirects. + await pushPref("dom.security.https_first", false); + + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 2); + await SpecialPowers.spawn( + tab.linkedBrowser, + [HTTPS_REDIRECT_SJS], + async function (url) { + content.wrappedJSObject.performRequests(1, url); + } + ); + await wait; + + is( + store.getState().requests.requests.length, + 2, + "There were two requests due to redirect." + ); + + const [ + initialDomainSecurityIcon, + initialUrlSecurityIcon, + redirectDomainSecurityIcon, + redirectUrlSecurityIcon, + ] = document.querySelectorAll(".requests-security-state-icon"); + + ok( + initialDomainSecurityIcon.classList.contains("security-state-insecure"), + "Initial request was marked insecure for domain column." + ); + + ok( + redirectDomainSecurityIcon.classList.contains("security-state-secure"), + "Redirected request was marked secure for domain column." + ); + + ok( + initialUrlSecurityIcon.classList.contains("security-state-insecure"), + "Initial request was marked insecure for URL column." + ); + + ok( + redirectUrlSecurityIcon.classList.contains("security-state-secure"), + "Redirected request was marked secure for URL column." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_security-state.js b/devtools/client/netmonitor/test/browser_net_security-state.js new file mode 100644 index 0000000000..29d40b3058 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_security-state.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that correct security state indicator appears depending on the security + * state. + */ + +add_task(async function () { + // This test explicitly asserts some insecure domains. + await pushPref("dom.security.https_first", false); + + const EXPECTED_SECURITY_STATES = { + "test1.example.com": "security-state-insecure", + "example.com": "security-state-secure", + "nocert.example.com": "security-state-broken", + localhost: "security-state-secure", + }; + + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + await performRequests(); + + for (const subitemNode of Array.from( + document.querySelectorAll(".requests-list-column.requests-list-domain") + )) { + // Skip header + const icon = subitemNode.querySelector(".requests-security-state-icon"); + if (!icon) { + continue; + } + + const domain = subitemNode.textContent; + info("Found a request to " + domain); + + const classes = icon.classList; + const expectedClass = EXPECTED_SECURITY_STATES[domain]; + + info("Classes of security state icon are: " + classes); + info("Security state icon is expected to contain class: " + expectedClass); + ok( + classes.contains(expectedClass), + "Icon contained the correct class name." + ); + } + + return teardown(monitor); + + /** + * A helper that performs requests to + * - https://nocert.example.com (broken) + * - https://example.com (secure) + * - http://test1.example.com (insecure) + * - http://localhost (local) + * and waits until NetworkMonitor has handled all packets sent by the server. + */ + async function performRequests() { + function executeRequests(count, url) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [{ count, url }], + async function (args) { + content.wrappedJSObject.performRequests(args.count, args.url); + } + ); + } + + let done = waitForNetworkEvents(monitor, 1); + info("Requesting a resource that has a certificate problem."); + await executeRequests(1, "https://nocert.example.com"); + + // Wait for the request to complete before firing another request. Otherwise + // the request with security issues interfere with waitForNetworkEvents. + info("Waiting for request to complete."); + await done; + + // Next perform a request over HTTP. If done the other way around the latter + // occasionally hangs waiting for event timings that don't seem to appear... + done = waitForNetworkEvents(monitor, 1); + info("Requesting a resource over HTTP."); + await executeRequests(1, "http://test1.example.com" + CORS_SJS_PATH); + await done; + + done = waitForNetworkEvents(monitor, 1); + info("Requesting a resource over HTTPS."); + await executeRequests(1, "https://example.com" + CORS_SJS_PATH); + await done; + + done = waitForNetworkEvents(monitor, 1); + info("Requesting a resource over HTTP to localhost."); + await executeRequests(1, "http://localhost" + CORS_SJS_PATH); + await done; + + const expectedCount = Object.keys(EXPECTED_SECURITY_STATES).length; + is( + store.getState().requests.requests.length, + expectedCount, + expectedCount + " events logged." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js b/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js new file mode 100644 index 0000000000..ac4750e564 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_security-tab-deselect.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that security details tab is no longer selected if an insecure request + * is selected. + */ + +add_task(async function () { + // This test needs to trigger http and https requests. + // Disable https-first to avoid blocking the HTTP request due to mixed content. + await pushPref("dom.security.https_first", false); + + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Performing requests."); + let wait = waitForNetworkEvents(monitor, 2); + const REQUEST_URLS = [ + "https://example.com" + CORS_SJS_PATH, + "http://example.com" + CORS_SJS_PATH, + ]; + await SpecialPowers.spawn( + tab.linkedBrowser, + [REQUEST_URLS], + async function (urls) { + for (const url of urls) { + content.wrappedJSObject.performRequests(1, url); + } + } + ); + await wait; + + info("Selecting secure request."); + wait = waitForDOM(document, ".tabs"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + info("Selecting security tab."); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelector("#security-tab") + ); + + info("Selecting insecure request."); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[1] + ); + + ok( + document.querySelector("#headers-tab[aria-selected=true]"), + "Selected tab was reset when selected security tab was hidden." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js new file mode 100644 index 0000000000..8ffc8efd50 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that security details tab is visible only when it should. + */ + +add_task(async function () { + // This test explicitly asserts some insecure domains. + await pushPref("dom.security.https_first", false); + + const TEST_DATA = [ + { + desc: "http request", + uri: "http://example.com" + CORS_SJS_PATH, + visibleOnNewEvent: false, + visibleOnSecurityInfo: false, + visibleOnceComplete: false, + securityState: "insecure", + }, + { + desc: "working https request", + uri: "https://example.com" + CORS_SJS_PATH, + visibleOnNewEvent: true, + visibleOnSecurityInfo: true, + visibleOnceComplete: true, + securityState: "secure", + }, + { + desc: "broken https request", + uri: "https://nocert.example.com", + isBroken: true, + visibleOnNewEvent: true, + visibleOnSecurityInfo: true, + visibleOnceComplete: true, + securityState: "broken", + }, + ]; + + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSelectedRequest } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + for (const testcase of TEST_DATA) { + info("Testing Security tab visibility for " + testcase.desc); + const onNewItem = monitor.panelWin.api.once(TEST_EVENTS.NETWORK_EVENT); + const onComplete = testcase.isBroken + ? waitForSecurityBrokenNetworkEvent() + : waitForNetworkEvents(monitor, 1); + + info("Performing a request to " + testcase.uri); + await SpecialPowers.spawn( + tab.linkedBrowser, + [testcase.uri], + async function (url) { + content.wrappedJSObject.performRequests(1, url); + } + ); + + info("Waiting for new network event."); + await onNewItem; + + info("Waiting for request to complete."); + await onComplete; + + info("Selecting the request."); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + + is( + getSelectedRequest(store.getState()).securityState, + testcase.securityState, + "Security state is immediately set" + ); + is( + !!document.querySelector("#security-tab"), + testcase.visibleOnNewEvent, + "Security tab is " + + (testcase.visibleOnNewEvent ? "visible" : "hidden") + + " after new request was added to the menu." + ); + + if (testcase.visibleOnSecurityInfo) { + // click security panel to lazy load the securityState + await waitUntil(() => document.querySelector("#security-tab")); + clickOnSidebarTab(document, "security"); + await waitUntil(() => + document.querySelector("#security-panel .security-info-value") + ); + info("Waiting for security information to arrive."); + + await waitUntil( + () => !!getSelectedRequest(store.getState()).securityState + ); + } + + is( + !!document.querySelector("#security-tab"), + testcase.visibleOnSecurityInfo, + "Security tab is " + + (testcase.visibleOnSecurityInfo ? "visible" : "hidden") + + " after security information arrived." + ); + + is( + !!document.querySelector("#security-tab"), + testcase.visibleOnceComplete, + "Security tab is " + + (testcase.visibleOnceComplete ? "visible" : "hidden") + + " after request has been completed." + ); + + await clearNetworkEvents(monitor); + } + + return teardown(monitor); + + /** + * Returns a promise that's resolved once a request with security issues is + * completed. + */ + function waitForSecurityBrokenNetworkEvent() { + const awaitedEvents = ["UPDATING_EVENT_TIMINGS", "RECEIVED_EVENT_TIMINGS"]; + + const promises = awaitedEvents.map(event => { + return monitor.panelWin.api.once(EVENTS[event]); + }); + + return Promise.all(promises); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_security-warnings.js b/devtools/client/netmonitor/test/browser_net_security-warnings.js new file mode 100644 index 0000000000..dfb6e9bef8 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_security-warnings.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that warning indicators are shown when appropriate. + */ + +const TEST_CASES = [ + { + desc: "no warnings", + uri: "https://example.com" + CORS_SJS_PATH, + warnCipher: null, + }, +]; + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + for (const test of TEST_CASES) { + info("Testing site with " + test.desc); + + info("Performing request to " + test.uri); + let wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [test.uri], + async function (url) { + content.wrappedJSObject.performRequests(1, url); + } + ); + await wait; + + info("Selecting the request."); + wait = waitForDOM(document, ".tabs"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + if (!document.querySelector("#security-tab[aria-selected=true]")) { + info("Selecting security tab."); + wait = waitForDOM(document, "#security-panel .properties-view"); + clickOnSidebarTab(document, "security"); + await wait; + } + + is( + document.querySelector("#security-warning-cipher"), + test.warnCipher, + "Cipher suite warning is hidden." + ); + + await clearNetworkEvents(monitor); + } + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js b/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js new file mode 100644 index 0000000000..900f5f48c8 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_send-beacon-other-tab.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if beacons from other tabs are properly ignored. + */ + +add_task(async function () { + const { monitor, tab } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + const { store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const beaconTab = await addTab(SEND_BEACON_URL); + info("Beacon tab added successfully."); + + is( + store.getState().requests.requests.length, + 0, + "The requests menu should be empty." + ); + + const wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(beaconTab.linkedBrowser, [], async function () { + content.wrappedJSObject.performRequests(); + }); + await reloadBrowser({ browser: tab.linkedBrowser }); + await wait; + + is( + store.getState().requests.requests.length, + 1, + "Only the reload should be recorded." + ); + const request = getSortedRequests(store.getState())[0]; + is(request.method, "GET", "The method is correct."); + is(request.status, "200", "The status is correct."); + + await removeTab(beaconTab); + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_send-beacon.js b/devtools/client/netmonitor/test/browser_net_send-beacon.js new file mode 100644 index 0000000000..d47299ca7f --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_send-beacon.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if beacons are handled correctly. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(SEND_BEACON_URL, { + requestCount: 1, + }); + const { store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + is( + store.getState().requests.requests.length, + 0, + "The requests menu should be empty." + ); + + // Execute requests. + await performRequests(monitor, tab, 1); + + is( + store.getState().requests.requests.length, + 1, + "The beacon should be recorded." + ); + + const request = getSortedRequests(store.getState())[0]; + is(request.method, "POST", "The method is correct."); + ok(request.url.endsWith("beacon_request"), "The URL is correct."); + is(request.status, "404", "The status is correct."); + is(request.blockedReason, 0, "The request is not blocked"); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_server_timings.js b/devtools/client/netmonitor/test/browser_net_server_timings.js new file mode 100644 index 0000000000..1ed635168f --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_server_timings.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if server side timings are displayed + */ +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CUSTOM_GET_URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [SERVER_TIMINGS_TYPE_SJS], + async function (url) { + content.wrappedJSObject.performRequests(1, url); + } + ); + await wait; + + // There must be 4 timing values (including server side timings). + const timingsSelector = "#timings-panel .tabpanel-summary-container.server"; + wait = waitForDOM(document, timingsSelector, 4); + + AccessibilityUtils.setEnv({ + // Keyboard users will will see the sidebar when the request row is + // selected. Accessibility is handled on the container level. + actionCountRule: false, + interactiveRule: false, + labelRule: false, + }); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelectorAll(".request-list-item")[0] + ); + AccessibilityUtils.resetEnv(); + + store.dispatch(Actions.toggleNetworkDetails()); + + clickOnSidebarTab(document, "timings"); + await wait; + + // Check the UI contains server side timings and correct values + const timings = document.querySelectorAll(timingsSelector, 4); + is( + timings[0].textContent, + "time1123 ms", + "The first server-timing must be correct" + ); + is( + timings[1].textContent, + "time20 ms", + "The second server-timing must be correct" + ); + is( + timings[2].textContent, + "time31.66 min", + "The third server-timing must be correct" + ); + is( + timings[3].textContent, + "time41.11 s", + "The fourth server-timing must be correct" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_service-worker-status.js b/devtools/client/netmonitor/test/browser_net_service-worker-status.js new file mode 100644 index 0000000000..bfeb2ba937 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_service-worker-status.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if requests intercepted by service workers have the correct status code + */ + +// Service workers only work on https +const URL = EXAMPLE_URL.replace("http:", "https:"); + +const TEST_URL = URL + "service-workers/status-codes.html"; + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(TEST_URL, { + enableCache: true, + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const REQUEST_DATA = [ + { + method: "GET", + uri: + URL + + "service-workers/sjs_content-type-test-server.sjs?sts=304&fmt=html", + details: { + status: 200, + statusText: "OK (service worker)", + displayedStatus: "service worker", + type: "plain", + fullMimeType: "text/plain; charset=UTF-8", + }, + stackFunctions: ["performRequests"], + }, + ]; + + info("Registering the service worker..."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.registerServiceWorker(); + }); + + info("Performing requests..."); + // Execute requests. + await performRequests(monitor, tab, REQUEST_DATA.length); + + // Fetch stack-trace data from the backend and wait till + // all packets are received. + const requests = getSortedRequests(store.getState()); + await Promise.all( + requests.map(requestItem => + connector.requestData(requestItem.id, "stackTrace") + ) + ); + + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + } + + let index = 0; + for (const request of REQUEST_DATA) { + const item = getSortedRequests(store.getState())[index]; + + info(`Verifying request #${index}`); + await verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + item, + request.method, + request.uri, + request.details + ); + + const { stacktrace } = item; + const stackLen = stacktrace ? stacktrace.length : 0; + + ok(stacktrace, `Request #${index} has a stacktrace`); + Assert.greaterOrEqual( + stackLen, + request.stackFunctions.length, + `Request #${index} has a stacktrace with enough (${stackLen}) items` + ); + + request.stackFunctions.forEach((functionName, j) => { + is( + stacktrace[j].functionName, + functionName, + `Request #${index} has the correct function at position #${j} on the stack` + ); + }); + + index++; + } + + info("Unregistering the service worker..."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.unregisterServiceWorker(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_service-worker-timings.js b/devtools/client/netmonitor/test/browser_net_service-worker-timings.js new file mode 100644 index 0000000000..2ae893703d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_service-worker-timings.js @@ -0,0 +1,72 @@ +"use strict"; + +/** + * Tests that timings for requests from the service workers are displayed correctly. + */ +add_task(async function testServiceWorkerTimings() { + // Service workers only work on https + const TEST_URL = HTTPS_EXAMPLE_URL + "service-workers/status-codes.html"; + const { tab, monitor } = await initNetMonitor(TEST_URL, { + enableCache: true, + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Registering the service worker..."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.registerServiceWorker(); + }); + + info("Performing requests which are intercepted by service worker..."); + await performRequests(monitor, tab, 1); + + const timingsSelector = + "#timings-panel .tabpanel-summary-container.service-worker"; + wait = waitForDOM(document, timingsSelector, 3); + + AccessibilityUtils.setEnv({ + // Keyboard users will will see the sidebar when the request row is + // selected. Accessibility is handled on the container level. + actionCountRule: false, + interactiveRule: false, + labelRule: false, + }); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelectorAll(".request-list-item")[0] + ); + AccessibilityUtils.resetEnv(); + + store.dispatch(Actions.toggleNetworkDetails()); + + clickOnSidebarTab(document, "timings"); + await wait; + + const timings = document.querySelectorAll(timingsSelector, 3); + // Note: The specific timing numbers change so this only asserts that the + // timings are visible + ok( + timings[0].textContent.includes("Startup"), + "The service worker timing for launch of the service worker is visible" + ); + ok( + timings[1].textContent.includes("Dispatch fetch:"), + "The service worker timing for dispatching the fetch event is visible" + ); + ok( + timings[2].textContent.includes("Handle fetch:"), + "The service worker timing for handling the fetch event is visible" + ); + + info("Unregistering the service worker..."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.unregisterServiceWorker(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_set-cookie-same-site.js b/devtools/client/netmonitor/test/browser_net_set-cookie-same-site.js new file mode 100644 index 0000000000..ecfa4bedcf --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_set-cookie-same-site.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if the 'Same site' cookie attribute is correctly set in the cookie panel + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SET_COOKIE_SAME_SITE_SJS, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + let wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + wait = waitForDOM(document, ".headers-overview"); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + clickOnSidebarTab(document, "cookies"); + + info("Checking the SameSite property"); + const expectedValues = [ + { + key: "foo", + value: "", + }, + { + key: "samesite", + value: '"Lax"', + }, + { + key: "value", + value: '"bar"', + }, + { + key: "foo", + value: '"bar"', + }, + ]; + const labelCells = document.querySelectorAll(".treeLabelCell"); + const valueCells = document.querySelectorAll(".treeValueCell"); + is( + valueCells.length, + labelCells.length, + "Number of labels " + + labelCells.length + + " different from number of values " + + valueCells.length + ); + + // Go through the cookie properties and check if each one has the expected + // label and value + for (let index = 0; index < labelCells.length; ++index) { + is( + labelCells[index].innerText, + expectedValues[index].key, + "Actual label " + + labelCells[index].innerText + + " not equal to expected label " + + expectedValues[index].key + ); + is( + valueCells[index].innerText, + expectedValues[index].value, + "Actual value " + + valueCells[index].innerText + + " not equal to expected value " + + expectedValues[index].value + ); + } + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_simple-request-data.js b/devtools/client/netmonitor/test/browser_net_simple-request-data.js new file mode 100644 index 0000000000..7b112ef7d8 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js @@ -0,0 +1,489 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if requests render correct information in the menu UI. + */ + +function test() { + // Disable tcp fast open, because it is setting a response header indicator + // (bug 1352274). TCP Fast Open is not present on all platforms therefore the + // number of response headers will vary depending on the platform. + Services.prefs.setBoolPref("network.tcp.tcp_fastopen_enable", false); + + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + initNetMonitor(SIMPLE_SJS, { requestCount: 1 }).then( + async ({ tab, monitor }) => { + info("Starting test... "); + + const { document, store, windowRequire, connector } = monitor.panelWin; + const { EVENTS, TEST_EVENTS } = windowRequire( + "devtools/client/netmonitor/src/constants" + ); + const { getDisplayedRequests, getSelectedRequest, getSortedRequests } = + windowRequire("devtools/client/netmonitor/src/selectors/index"); + + const promiseList = []; + promiseList.push(waitForNetworkEvents(monitor, 1)); + + function expectEvent(evt, cb) { + promiseList.push( + new Promise((resolve, reject) => { + monitor.panelWin.api.once(evt, _ => { + cb().then(resolve, reject); + }); + }) + ); + } + + expectEvent(TEST_EVENTS.NETWORK_EVENT, async () => { + is( + getSelectedRequest(store.getState()), + undefined, + "There shouldn't be any selected item in the requests menu." + ); + is( + store.getState().requests.requests.length, + 1, + "The requests menu should not be empty after the first request." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The network details panel should still be hidden after first request." + ); + + const requestItem = getSortedRequests(store.getState())[0]; + + is( + typeof requestItem.id, + "string", + "The attached request id is incorrect." + ); + isnot( + requestItem.id, + "", + "The attached request id should not be empty." + ); + + is( + typeof requestItem.startedMs, + "number", + "The attached startedMs is incorrect." + ); + isnot( + requestItem.startedMs, + 0, + "The attached startedMs should not be zero." + ); + + /* + * Bug 1666495: this is not possible to assert not yet set attributes + * because of throttling, which only updates the frontend after a few attributes + * are already retrieved via onResourceUpdates events. + * This test should be tweaked with slow responding requests in order to assert + * such behavior without disabling throttling. + + is( + requestItem.requestHeaders, + undefined, + "The requestHeaders should not yet be set." + ); + is( + requestItem.requestCookies, + undefined, + "The requestCookies should not yet be set." + ); + is( + requestItem.requestPostData, + undefined, + "The requestPostData should not yet be set." + ); + + is( + requestItem.responseHeaders, + undefined, + "The responseHeaders should not yet be set." + ); + is( + requestItem.responseCookies, + undefined, + "The responseCookies should not yet be set." + ); + + is( + requestItem.httpVersion, + undefined, + "The httpVersion should not yet be set." + ); + is(requestItem.status, undefined, "The status should not yet be set."); + is( + requestItem.statusText, + undefined, + "The statusText should not yet be set." + ); + + is( + requestItem.headersSize, + undefined, + "The headersSize should not yet be set." + ); + is( + requestItem.transferredSize, + undefined, + "The transferredSize should not yet be set." + ); + is( + requestItem.contentSize, + undefined, + "The contentSize should not yet be set." + ); + + is( + requestItem.responseContent, + undefined, + "The responseContent should not yet be set." + ); + + is( + requestItem.totalTime, + undefined, + "The totalTime should not yet be set." + ); + is( + requestItem.eventTimings, + undefined, + "The eventTimings should not yet be set." + ); + */ + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS + ); + }); + + expectEvent(TEST_EVENTS.RECEIVED_REQUEST_HEADERS, async () => { + await waitForRequestData(store, ["requestHeaders"]); + + const requestItem = getSortedRequests(store.getState())[0]; + + ok( + requestItem.requestHeaders, + "There should be a requestHeaders data available." + ); + is( + requestItem.requestHeaders.headers.length, + 10, + "The requestHeaders data has an incorrect |headers| property." + ); + isnot( + requestItem.requestHeaders.headersSize, + 0, + "The requestHeaders data has an incorrect |headersSize| property." + ); + // Can't test for the exact request headers size because the value may + // vary across platforms ("User-Agent" header differs). + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS + ); + }); + + expectEvent(TEST_EVENTS.RECEIVED_REQUEST_COOKIES, async () => { + await waitForRequestData(store, ["requestCookies"]); + + const requestItem = getSortedRequests(store.getState())[0]; + + ok( + requestItem.requestCookies, + "There should be a requestCookies data available." + ); + is( + requestItem.requestCookies.length, + 2, + "The requestCookies data has an incorrect |cookies| property." + ); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS + ); + }); + + monitor.panelWin.api.once(TEST_EVENTS.RECEIVED_REQUEST_POST_DATA, () => { + ok(false, "Trap listener: this request doesn't have any post data."); + }); + + expectEvent(TEST_EVENTS.RECEIVED_RESPONSE_HEADERS, async () => { + await waitForRequestData(store, ["responseHeaders"]); + + const requestItem = getSortedRequests(store.getState())[0]; + + ok( + requestItem.responseHeaders, + "There should be a responseHeaders data available." + ); + is( + requestItem.responseHeaders.headers.length, + 13, + "The responseHeaders data has an incorrect |headers| property." + ); + is( + requestItem.responseHeaders.headersSize, + 335, + "The responseHeaders data has an incorrect |headersSize| property." + ); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS + ); + }); + + expectEvent(TEST_EVENTS.RECEIVED_RESPONSE_COOKIES, async () => { + await waitForRequestData(store, ["responseCookies"]); + + const requestItem = getSortedRequests(store.getState())[0]; + + ok( + requestItem.responseCookies, + "There should be a responseCookies data available." + ); + is( + requestItem.responseCookies.length, + 2, + "The responseCookies data has an incorrect |cookies| property." + ); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS + ); + }); + + expectEvent(TEST_EVENTS.STARTED_RECEIVING_RESPONSE, async () => { + await waitForRequestData(store, [ + "httpVersion", + "status", + "statusText", + "headersSize", + ]); + + const requestItem = getSortedRequests(store.getState())[0]; + + is( + requestItem.httpVersion, + "HTTP/1.1", + "The httpVersion data has an incorrect value." + ); + is( + requestItem.status, + "200", + "The status data has an incorrect value." + ); + is( + requestItem.statusText, + "Och Aye", + "The statusText data has an incorrect value." + ); + is( + requestItem.headersSize, + 335, + "The headersSize data has an incorrect value." + ); + + const requestListItem = document.querySelector(".request-list-item"); + requestListItem.scrollIntoView(); + const requestsListStatus = + requestListItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded( + requestListItem, + ".requests-list-timings-total" + ); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS, + { + status: "200", + statusText: "Och Aye", + } + ); + }); + + expectEvent(EVENTS.PAYLOAD_READY, async () => { + await waitForRequestData(store, [ + "transferredSize", + "contentSize", + "mimeType", + ]); + + const requestItem = getSortedRequests(store.getState())[0]; + + is( + requestItem.transferredSize, + 347, + "The transferredSize data has an incorrect value." + ); + is( + requestItem.contentSize, + 12, + "The contentSize data has an incorrect value." + ); + is( + requestItem.mimeType, + "text/plain; charset=utf-8", + "The mimeType data has an incorrect value." + ); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS, + { + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 347), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12), + } + ); + }); + + expectEvent(EVENTS.UPDATING_EVENT_TIMINGS, async () => { + await waitForRequestData(store, ["eventTimings"]); + + const requestItem = getSortedRequests(store.getState())[0]; + + is( + typeof requestItem.totalTime, + "number", + "The attached totalTime is incorrect." + ); + Assert.greaterOrEqual( + requestItem.totalTime, + 0, + "The attached totalTime should be positive." + ); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS, + { + time: true, + } + ); + }); + + expectEvent(EVENTS.RECEIVED_EVENT_TIMINGS, async () => { + await waitForRequestData(store, ["eventTimings"]); + + const requestItem = getSortedRequests(store.getState())[0]; + + ok( + requestItem.eventTimings, + "There should be a eventTimings data available." + ); + is( + typeof requestItem.eventTimings.timings.blocked, + "number", + "The eventTimings data has an incorrect |timings.blocked| property." + ); + is( + typeof requestItem.eventTimings.timings.dns, + "number", + "The eventTimings data has an incorrect |timings.dns| property." + ); + is( + typeof requestItem.eventTimings.timings.ssl, + "number", + "The eventTimings data has an incorrect |timings.ssl| property." + ); + is( + typeof requestItem.eventTimings.timings.connect, + "number", + "The eventTimings data has an incorrect |timings.connect| property." + ); + is( + typeof requestItem.eventTimings.timings.send, + "number", + "The eventTimings data has an incorrect |timings.send| property." + ); + is( + typeof requestItem.eventTimings.timings.wait, + "number", + "The eventTimings data has an incorrect |timings.wait| property." + ); + is( + typeof requestItem.eventTimings.timings.receive, + "number", + "The eventTimings data has an incorrect |timings.receive| property." + ); + is( + typeof requestItem.eventTimings.totalTime, + "number", + "The eventTimings data has an incorrect |totalTime| property." + ); + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + "GET", + SIMPLE_SJS, + { + time: true, + } + ); + }); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + const requestItem = getSortedRequests(store.getState())[0]; + + if (!requestItem.requestHeaders) { + connector.requestData(requestItem.id, "requestHeaders"); + } + if (!requestItem.responseHeaders) { + connector.requestData(requestItem.id, "responseHeaders"); + } + + await Promise.all(promiseList); + await teardown(monitor); + finish(); + } + ); +} diff --git a/devtools/client/netmonitor/test/browser_net_simple-request-details.js b/devtools/client/netmonitor/test/browser_net_simple-request-details.js new file mode 100644 index 0000000000..e52a0b101a --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_simple-request-details.js @@ -0,0 +1,388 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if requests render correct information in the details UI. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(SIMPLE_SJS, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { PANELS } = windowRequire("devtools/client/netmonitor/src/constants"); + const { getSelectedRequest, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + is( + getSelectedRequest(store.getState()), + undefined, + "There shouldn't be any selected item in the requests menu." + ); + is( + store.getState().requests.requests.length, + 1, + "The requests menu should not be empty after the first request." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The network details panel should still be hidden after first request." + ); + + const waitForHeaders = waitForDOM(document, ".headers-overview"); + + store.dispatch(Actions.toggleNetworkDetails()); + + isnot( + getSelectedRequest(store.getState()), + undefined, + "There should be a selected item in the requests menu." + ); + is( + getSelectedIndex(store.getState()), + 0, + "The first item should be selected in the requests menu." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should not be hidden after toggle button was pressed." + ); + + await waitForHeaders; + + await testHeadersTab(); + await testCookiesTab(); + await testParamsTab(); + await testResponseTab(); + await testTimingsTab(); + await closePanelOnEsc(); + return teardown(monitor); + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } + + async function testHeadersTab() { + const tabEl = document.querySelectorAll( + ".network-details-bar .tabs-menu a" + )[0]; + const tabpanel = document.querySelector("#headers-panel"); + + is( + tabEl.getAttribute("aria-selected"), + "true", + "The headers tab in the network details pane should be selected." + ); + // Request URL + is( + tabpanel.querySelector(".url-preview .url").innerText, + SIMPLE_SJS, + "The url summary value is incorrect." + ); + + // Request method + is( + tabpanel.querySelectorAll(".treeLabel")[0].innerText, + "GET", + "The method summary value is incorrect." + ); + // Status code + is( + tabpanel.querySelector(".requests-list-status-code").innerText, + "200", + "The status summary code is incorrect." + ); + is( + tabpanel.querySelector(".status").childNodes[1].textContent, + "Och Aye", + "The status summary value is incorrect." + ); + // Version + is( + tabpanel.querySelectorAll(".tabpanel-summary-value")[1].innerText, + "HTTP/1.1", + "The HTTP version is incorrect." + ); + + await waitForRequestData(store, ["requestHeaders", "responseHeaders"]); + + is( + tabpanel.querySelectorAll(".accordion-item").length, + 2, + "There should be 2 header scopes displayed in this tabpanel." + ); + + is( + tabpanel.querySelectorAll(".accordion .treeLabelCell").length, + 23, + "There should be 23 header values displayed in this tabpanel." + ); + + const headersTable = tabpanel.querySelector(".accordion"); + const responseScope = headersTable.querySelectorAll( + "tr[id^='/Response Headers']" + ); + const requestScope = headersTable.querySelectorAll( + "tr[id^='/Request Headers']" + ); + + const headerLabels = headersTable.querySelectorAll( + ".accordion-item .accordion-header-label" + ); + + ok( + headerLabels[0].innerHTML.match( + new RegExp(L10N.getStr("responseHeaders") + " \\([0-9]+ .+\\)") + ), + "The response headers scope doesn't have the correct title." + ); + + ok( + headerLabels[1].innerHTML.includes(L10N.getStr("requestHeaders") + " ("), + "The request headers scope doesn't have the correct title." + ); + + const responseHeaders = [ + { + name: "cache-control", + value: "no-cache, no-store, must-revalidate", + pos: "first", + index: 1, + }, + { + name: "connection", + value: "close", + pos: "second", + index: 2, + }, + { + name: "content-length", + value: "12", + pos: "third", + index: 3, + }, + { + name: "content-type", + value: "text/plain; charset=utf-8", + pos: "fourth", + index: 4, + }, + { + name: "foo-bar", + value: "baz", + pos: "seventh", + index: 7, + }, + ]; + responseHeaders.forEach(header => { + is( + responseScope[header.index - 1].querySelector(".treeLabel").innerHTML, + header.name, + `The ${header.pos} response header name was incorrect.` + ); + is( + responseScope[header.index - 1].querySelector(".objectBox").innerHTML, + `${header.value}`, + `The ${header.pos} response header value was incorrect.` + ); + }); + + const requestHeaders = [ + { + name: "Cache-Control", + value: "no-cache", + pos: "fourth", + index: 4, + }, + { + name: "Connection", + value: "keep-alive", + pos: "fifth", + index: 5, + }, + { + name: "Host", + value: "example.com", + pos: "seventh", + index: 7, + }, + { + name: "Pragma", + value: "no-cache", + pos: "eighth", + index: 8, + }, + ]; + requestHeaders.forEach(header => { + is( + requestScope[header.index - 1].querySelector(".treeLabel").innerHTML, + header.name, + `The ${header.pos} request header name was incorrect.` + ); + is( + requestScope[header.index - 1].querySelector(".objectBox").innerHTML, + `${header.value}`, + `The ${header.pos} request header value was incorrect.` + ); + }); + } + + async function testCookiesTab() { + const tabpanel = await selectTab(PANELS.COOKIES, 1); + + const cookieAccordion = tabpanel.querySelector(".accordion"); + + is( + cookieAccordion.querySelectorAll(".accordion-item").length, + 2, + "There should be 2 cookie scopes displayed in this tabpanel." + ); + // 2 Cookies in response - 1 httpOnly and 1 value for each cookie - total 6 + + const resCookiesTable = cookieAccordion.querySelector( + "li[id='responseCookies'] .accordion-content .treeTable" + ); + is( + resCookiesTable.querySelectorAll("tr.treeRow").length, + 6, + "There should be 6 rows displayed in response cookies table" + ); + + const reqCookiesTable = cookieAccordion.querySelector( + "li[id='requestCookies'] .accordion-content .treeTable" + ); + is( + reqCookiesTable.querySelectorAll("tr.treeRow").length, + 2, + "There should be 2 cookie values displayed in request cookies table." + ); + } + + async function testParamsTab() { + const tabpanel = await selectTab(PANELS.REQUEST, 2); + + is( + tabpanel.querySelectorAll(".panel-container").length, + 0, + "There should be no param scopes displayed in this tabpanel." + ); + is( + tabpanel.querySelectorAll(".empty-notice").length, + 1, + "The empty notice should be displayed in this tabpanel." + ); + } + + async function testResponseTab() { + const tabpanel = await selectTab(PANELS.RESPONSE, 3); + await waitForDOM(document, "#response-panel .source-editor-mount"); + + is( + tabpanel.querySelectorAll( + "#response-panel .raw-data-toggle-input .devtools-checkbox-toggle" + ).length, + 0, + "The raw data toggle should not be shown in this tabpanel." + ); + is( + tabpanel.querySelectorAll(".source-editor-mount").length, + 1, + "The response payload should be shown initially." + ); + } + + async function testTimingsTab() { + const tabpanel = await selectTab(PANELS.TIMINGS, 4); + + const displayFormat = new RegExp(/[0-9]+ ms$/); + const propsToVerify = [ + "blocked", + "dns", + "connect", + "ssl", + "send", + "wait", + "receive", + ]; + + // To ensure that test case for a new property is written, otherwise this + // test will fail + is( + tabpanel.querySelectorAll(".tabpanel-summary-container").length, + propsToVerify.length, + `There should be exactly ${propsToVerify.length} values + displayed in this tabpanel` + ); + + propsToVerify.forEach(propName => { + ok( + tabpanel + .querySelector( + `#timings-summary-${propName} + .requests-list-timings-total` + ) + .innerHTML.match(displayFormat), + `The ${propName} timing info does not appear to be correct.` + ); + }); + } + + async function selectTab(tabName, pos) { + const tabEl = document.querySelectorAll( + ".network-details-bar .tabs-menu a" + )[pos]; + + const onPanelOpen = waitForDOM(document, `#${tabName}-panel`); + clickOnSidebarTab( + document, + tabEl.id.substring(0, tabEl.id.indexOf("-tab")) + ); + await onPanelOpen; + + is( + tabEl.getAttribute("aria-selected"), + "true", + `The ${tabName} tab in the network details pane should be selected.` + ); + + return document.querySelector(".network-details-bar .tab-panel"); + } + + // This test will timeout on failure + async function closePanelOnEsc() { + EventUtils.sendKey("ESCAPE", window); + + await waitUntil(() => { + return document.querySelector(".network-details-bar") == null; + }); + + is( + document.querySelectorAll(".network-details-bar").length, + 0, + "Network details panel should close on ESC key" + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_simple-request.js b/devtools/client/netmonitor/test/browser_net_simple-request.js new file mode 100644 index 0000000000..8f5af6f600 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_simple-request.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test whether the UI state properly reflects existence of requests + * displayed in the Net panel. The following parts of the UI are + * tested: + * 1) Side panel visibility + * 2) Side panel toggle button + * 3) Empty user message visibility + * 4) Number of requests displayed + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + ok( + document.querySelector(".request-list-empty-notice"), + "An empty notice should be displayed when the frontend is opened." + ); + is( + store.getState().requests.requests.length, + 0, + "The requests menu should be empty when the frontend is opened." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The network details panel should be hidden when the frontend is opened." + ); + + await reloadAndWait(); + + ok( + !document.querySelector(".request-list-empty-notice"), + "The empty notice should be hidden after the first request." + ); + is( + store.getState().requests.requests.length, + 1, + "The requests menu should not be empty after the first request." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The network details panel should still be hidden after the first request." + ); + + await reloadAndWait(); + + ok( + !document.querySelector(".request-list-empty-notice"), + "The empty notice should be still hidden after a reload." + ); + is( + store.getState().requests.requests.length, + 1, + "The requests menu should not be empty after a reload." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The network details panel should still be hidden after a reload." + ); + + await clearNetworkEvents(monitor); + + ok( + document.querySelector(".request-list-empty-notice"), + "An empty notice should be displayed again after clear." + ); + is( + store.getState().requests.requests.length, + 0, + "The requests menu should be empty after clear." + ); + is( + !!document.querySelector(".network-details-bar"), + false, + "The network details panel should still be hidden after clear." + ); + + return teardown(monitor); + + async function reloadAndWait() { + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + return wait; + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_sort-01.js b/devtools/client/netmonitor/test/browser_net_sort-01.js new file mode 100644 index 0000000000..8d2009a7db --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_sort-01.js @@ -0,0 +1,316 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if sorting columns in the network table works correctly with new requests. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(SORTING_URL, { requestCount: 1 }); + info("Starting test... "); + + // It seems that this test may be slow on debug builds. This could be because + // of the heavy dom manipulation associated with sorting. + requestLongerTimeout(2); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSelectedRequest, getSortedRequests } = + windowRequire("devtools/client/netmonitor/src/selectors/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Loading the frame script and preparing the xhr request URLs so we can + // generate some requests later. + const requests = [ + { + url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(), + method: "GET1", + }, + { + url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(), + method: "GET5", + }, + { + url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(), + method: "GET2", + }, + { + url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(), + method: "GET4", + }, + { + url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(), + method: "GET3", + }, + ]; + + let wait = waitForNetworkEvents(monitor, 5); + await performRequestsInContent(requests); + await wait; + + store.dispatch(Actions.toggleNetworkDetails()); + + isnot( + getSelectedRequest(store.getState()), + undefined, + "There should be a selected item in the requests menu." + ); + is( + getSelectedIndex(store.getState()), + 0, + "The first item should be selected in the requests menu." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should be visible after toggle button was pressed." + ); + + testHeaders(); + await testContents([0, 2, 4, 3, 1], 0); + + info("Testing status sort, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + testHeaders("status", "ascending"); + await testContents([0, 1, 2, 3, 4], 0); + + info("Performing more requests."); + wait = waitForNetworkEvents(monitor, 5); + await performRequestsInContent(requests); + await wait; + + info("Testing status sort again, ascending."); + testHeaders("status", "ascending"); + await testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0); + + info("Testing status sort, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + testHeaders("status", "descending"); + await testContents([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 9); + + info("Performing more requests."); + wait = waitForNetworkEvents(monitor, 5); + await performRequestsInContent(requests); + await wait; + + info("Testing status sort again, descending."); + testHeaders("status", "descending"); + await testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14); + + info("Testing status sort yet again, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + testHeaders("status", "ascending"); + await testContents([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], 0); + + info("Testing status sort yet again, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + testHeaders("status", "descending"); + await testContents([14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0], 14); + + return teardown(monitor); + + function testHeaders(sortType, direction) { + const doc = monitor.panelWin.document; + const target = doc.querySelector("#requests-list-" + sortType + "-button"); + const headers = doc.querySelectorAll(".requests-list-header-button"); + + for (const header of headers) { + if (header != target) { + ok( + !header.hasAttribute("data-sorted"), + "The " + + header.id + + " header does not have a 'data-sorted' attribute." + ); + ok( + !header + .getAttribute("title") + .includes(L10N.getStr("networkMenu.sortedAsc")) && + !header + .getAttribute("title") + .includes(L10N.getStr("networkMenu.sortedDesc")), + "The " + + header.id + + " header does not include any sorting in the 'title' attribute." + ); + } else { + is( + header.getAttribute("data-sorted"), + direction, + "The " + header.id + " header has a correct 'data-sorted' attribute." + ); + const sorted = + direction == "ascending" + ? L10N.getStr("networkMenu.sortedAsc") + : L10N.getStr("networkMenu.sortedDesc"); + ok( + header.getAttribute("title").includes(sorted), + "The " + + header.id + + " header includes the used sorting in the 'title' attribute." + ); + } + } + } + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } + + async function testContents(order, selection) { + isnot( + getSelectedRequest(store.getState()), + undefined, + "There should still be a selected item after sorting." + ); + is( + getSelectedIndex(store.getState()), + selection, + "The first item should be still selected after sorting." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should still be visible after sorting." + ); + + is( + getSortedRequests(store.getState()).length, + order.length, + "There should be a specific number of items in the requests menu." + ); + is( + getDisplayedRequests(store.getState()).length, + order.length, + "There should be a specific number of visbile items in the requests menu." + ); + is( + document.querySelectorAll(".request-list-item").length, + order.length, + "The visible items in the requests menu are, in fact, visible!" + ); + + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + } + + for (let i = 0, len = order.length / 5; i < len; i++) { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[order[i]], + "GET1", + SORTING_SJS + "?index=1", + { + fuzzyUrl: true, + status: 101, + statusText: "Meh", + type: "1", + fullMimeType: "text/1", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 198), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0), + } + ); + } + for (let i = 0, len = order.length / 5; i < len; i++) { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[order[i + len]], + "GET2", + SORTING_SJS + "?index=2", + { + fuzzyUrl: true, + status: 200, + statusText: "Meh", + type: "2", + fullMimeType: "text/2", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 217), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19), + } + ); + } + for (let i = 0, len = order.length / 5; i < len; i++) { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[order[i + len * 2]], + "GET3", + SORTING_SJS + "?index=3", + { + fuzzyUrl: true, + status: 300, + statusText: "Meh", + type: "3", + fullMimeType: "text/3", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 227), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29), + } + ); + } + for (let i = 0, len = order.length / 5; i < len; i++) { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[order[i + len * 3]], + "GET4", + SORTING_SJS + "?index=4", + { + fuzzyUrl: true, + status: 400, + statusText: "Meh", + type: "4", + fullMimeType: "text/4", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 237), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39), + } + ); + } + for (let i = 0, len = order.length / 5; i < len; i++) { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[order[i + len * 4]], + "GET5", + SORTING_SJS + "?index=5", + { + fuzzyUrl: true, + status: 500, + statusText: "Meh", + type: "5", + fullMimeType: "text/5", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 247), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49), + } + ); + } + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_sort-02.js b/devtools/client/netmonitor/test/browser_net_sort-02.js new file mode 100644 index 0000000000..5a2ae472fc --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_sort-02.js @@ -0,0 +1,460 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if sorting columns in the network table works correctly. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(SORTING_URL, { requestCount: 1 }); + info("Starting test... "); + + // It seems that this test may be slow on debug builds. This could be because + // of the heavy dom manipulation associated with sorting. + requestLongerTimeout(2); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSelectedRequest, getSortedRequests } = + windowRequire("devtools/client/netmonitor/src/selectors/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Loading the frame script and preparing the xhr request URLs so we can + // generate some requests later. + const requests = [ + { + url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(), + method: "GET1", + }, + { + url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(), + method: "GET5", + }, + { + url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(), + method: "GET2", + }, + { + url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(), + method: "GET4", + }, + { + url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(), + method: "GET3", + }, + ]; + + const wait = waitForNetworkEvents(monitor, 5); + await performRequestsInContent(requests); + await wait; + + store.dispatch(Actions.toggleNetworkDetails()); + + isnot( + getSelectedRequest(store.getState()), + undefined, + "There should be a selected item in the requests menu." + ); + is( + getSelectedIndex(store.getState()), + 0, + "The first item should be selected in the requests menu." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should be visible after toggle button was pressed." + ); + + testHeaders(); + await testContents([0, 2, 4, 3, 1]); + + info("Testing status sort, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + testHeaders("status", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing status sort, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + testHeaders("status", "descending"); + await testContents([4, 3, 2, 1, 0]); + + info("Testing status sort, ascending. Checking sort loops correctly."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + testHeaders("status", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing method sort, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-method-button") + ); + testHeaders("method", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing method sort, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-method-button") + ); + testHeaders("method", "descending"); + await testContents([4, 3, 2, 1, 0]); + + info("Testing method sort, ascending. Checking sort loops correctly."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-method-button") + ); + testHeaders("method", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing file sort, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-file-button") + ); + testHeaders("file", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing file sort, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-file-button") + ); + testHeaders("file", "descending"); + await testContents([4, 3, 2, 1, 0]); + + info("Testing file sort, ascending. Checking sort loops correctly."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-file-button") + ); + testHeaders("file", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing URL sort, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-url-button") + ); + testHeaders("url", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing URL sort, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-url-button") + ); + testHeaders("url", "descending"); + await testContents([4, 3, 2, 1, 0]); + + info("Testing URL sort, ascending. Checking sort loops correctly."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-url-button") + ); + testHeaders("url", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing type sort, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-type-button") + ); + testHeaders("type", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing type sort, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-type-button") + ); + testHeaders("type", "descending"); + await testContents([4, 3, 2, 1, 0]); + + info("Testing type sort, ascending. Checking sort loops correctly."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-type-button") + ); + testHeaders("type", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing transferred sort, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-transferred-button") + ); + testHeaders("transferred", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing transferred sort, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-transferred-button") + ); + testHeaders("transferred", "descending"); + await testContents([4, 3, 2, 1, 0]); + + info("Testing transferred sort, ascending. Checking sort loops correctly."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-transferred-button") + ); + testHeaders("transferred", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing size sort, ascending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-contentSize-button") + ); + testHeaders("contentSize", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing size sort, descending."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-contentSize-button") + ); + testHeaders("contentSize", "descending"); + await testContents([4, 3, 2, 1, 0]); + + info("Testing size sort, ascending. Checking sort loops correctly."); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-contentSize-button") + ); + testHeaders("contentSize", "ascending"); + await testContents([0, 1, 2, 3, 4]); + + info("Testing waterfall sort, ascending."); + + // Because the waterfall column is hidden when the network details panel is + // opened, the waterfall button is not visible. Therefore we hide the network + // details panel + await store.dispatch(Actions.toggleNetworkDetails()); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-waterfall-button") + ); + await store.dispatch(Actions.toggleNetworkDetails()); + testHeaders("waterfall", "ascending"); + await testContents([0, 2, 4, 3, 1]); + + info("Testing waterfall sort, descending."); + await store.dispatch(Actions.toggleNetworkDetails()); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-waterfall-button") + ); + testHeaders("waterfall", "descending"); + await store.dispatch(Actions.toggleNetworkDetails()); + await testContents([4, 2, 0, 1, 3], true); + + info("Testing waterfall sort, ascending. Checking sort loops correctly."); + await store.dispatch(Actions.toggleNetworkDetails()); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-waterfall-button") + ); + testHeaders("waterfall", "ascending"); + await store.dispatch(Actions.toggleNetworkDetails()); + await testContents([0, 2, 4, 3, 1]); + + return teardown(monitor); + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } + + function testHeaders(sortType, direction) { + const doc = monitor.panelWin.document; + const target = doc.querySelector("#requests-list-" + sortType + "-button"); + const headers = doc.querySelectorAll(".requests-list-header-button"); + + for (const header of headers) { + if (header != target) { + ok( + !header.hasAttribute("data-sorted"), + "The " + + header.id + + " header does not have a 'data-sorted' attribute." + ); + ok( + !header + .getAttribute("title") + .includes(L10N.getStr("networkMenu.sortedAsc")) && + !header + .getAttribute("title") + .includes(L10N.getStr("networkMenu.sortedDesc")), + "The " + + header.id + + " header does not include any sorting in the 'title' attribute." + ); + } else { + is( + header.getAttribute("data-sorted"), + direction, + "The " + header.id + " header has a correct 'data-sorted' attribute." + ); + const sorted = + direction == "ascending" + ? L10N.getStr("networkMenu.sortedAsc") + : L10N.getStr("networkMenu.sortedDesc"); + ok( + header.getAttribute("title").includes(sorted), + "The " + + header.id + + " header includes the used sorting in the 'title' attribute." + ); + } + } + } + + async function testContents([a, b, c, d, e], waterfall = false) { + isnot( + getSelectedRequest(store.getState()), + undefined, + "There should still be a selected item after sorting." + ); + if (!waterfall) { + is( + getSelectedIndex(store.getState()), + a, + "The first item should be still selected after sorting." + ); + } + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should still be visible after sorting." + ); + + is( + getSortedRequests(store.getState()).length, + 5, + "There should be a total of 5 items in the requests menu." + ); + is( + getDisplayedRequests(store.getState()).length, + 5, + "There should be a total of 5 visible items in the requests menu." + ); + is( + document.querySelectorAll(".request-list-item").length, + 5, + "The visible items in the requests menu are, in fact, visible!" + ); + + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + } + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[a], + "GET1", + SORTING_SJS + "?index=1", + { + fuzzyUrl: true, + status: 101, + statusText: "Meh", + type: "1", + fullMimeType: "text/1", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 198), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0), + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[b], + "GET2", + SORTING_SJS + "?index=2", + { + fuzzyUrl: true, + status: 200, + statusText: "Meh", + type: "2", + fullMimeType: "text/2", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 217), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19), + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[c], + "GET3", + SORTING_SJS + "?index=3", + { + fuzzyUrl: true, + status: 300, + statusText: "Meh", + type: "3", + fullMimeType: "text/3", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 227), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29), + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[d], + "GET4", + SORTING_SJS + "?index=4", + { + fuzzyUrl: true, + status: 400, + statusText: "Meh", + type: "4", + fullMimeType: "text/4", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 237), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39), + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[e], + "GET5", + SORTING_SJS + "?index=5", + { + fuzzyUrl: true, + status: 500, + statusText: "Meh", + type: "5", + fullMimeType: "text/5", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 247), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49), + } + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_sort-reset.js b/devtools/client/netmonitor/test/browser_net_sort-reset.js new file mode 100644 index 0000000000..f2b58e203d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_sort-reset.js @@ -0,0 +1,284 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if sorting columns in the network table works correctly. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(SORTING_URL, { requestCount: 1 }); + info("Starting test... "); + + // It seems that this test may be slow on debug builds. This could be because + // of the heavy dom manipulation associated with sorting. + requestLongerTimeout(2); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSelectedRequest, getSortedRequests } = + windowRequire("devtools/client/netmonitor/src/selectors/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Loading the frame script and preparing the xhr request URLs so we can + // generate some requests later. + const requests = [ + { + url: "sjs_sorting-test-server.sjs?index=1&" + Math.random(), + method: "GET1", + }, + { + url: "sjs_sorting-test-server.sjs?index=5&" + Math.random(), + method: "GET5", + }, + { + url: "sjs_sorting-test-server.sjs?index=2&" + Math.random(), + method: "GET2", + }, + { + url: "sjs_sorting-test-server.sjs?index=4&" + Math.random(), + method: "GET4", + }, + { + url: "sjs_sorting-test-server.sjs?index=3&" + Math.random(), + method: "GET3", + }, + ]; + + const wait = waitForNetworkEvents(monitor, 5); + await performRequestsInContent(requests); + await wait; + + store.dispatch(Actions.toggleNetworkDetails()); + + testHeaders(); + await testContents([0, 2, 4, 3, 1]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + info("Testing sort reset using middle click."); + EventUtils.sendMouseEvent( + { type: "click", button: 1 }, + document.querySelector("#requests-list-status-button") + ); + testHeaders(); + await testContents([0, 2, 4, 3, 1]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + info("Testing sort reset using context menu 'Reset Sorting'"); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector("#requests-list-contentSize-button") + ); + await selectContextMenuItem(monitor, "request-list-header-reset-sorting"); + testHeaders(); + await testContents([0, 2, 4, 3, 1]); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#requests-list-status-button") + ); + info("Testing sort reset using context menu 'Reset Columns'"); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector("#requests-list-contentSize-button") + ); + await selectContextMenuItem(monitor, "request-list-header-reset-columns"); + testHeaders(); + // add columns because verifyRequestItemTarget expects some extra columns + await showColumn(monitor, "protocol"); + await showColumn(monitor, "remoteip"); + await showColumn(monitor, "scheme"); + await showColumn(monitor, "duration"); + await showColumn(monitor, "latency"); + await testContents([0, 2, 4, 3, 1]); + + return teardown(monitor); + + function getSelectedIndex(state) { + if (!state.requests.selectedId) { + return -1; + } + return getSortedRequests(state).findIndex( + r => r.id === state.requests.selectedId + ); + } + + function testHeaders(sortType, direction) { + const doc = monitor.panelWin.document; + const target = doc.querySelector("#requests-list-" + sortType + "-button"); + const headers = doc.querySelectorAll(".requests-list-header-button"); + + for (const header of headers) { + if (header != target) { + ok( + !header.hasAttribute("data-sorted"), + "The " + + header.id + + " header does not have a 'data-sorted' attribute." + ); + ok( + !header + .getAttribute("title") + .includes(L10N.getStr("networkMenu.sortedAsc")) && + !header + .getAttribute("title") + .includes(L10N.getStr("networkMenu.sortedDesc")), + "The " + + header.id + + " header does not include any sorting in the 'title' attribute." + ); + } else { + is( + header.getAttribute("data-sorted"), + direction, + "The " + header.id + " header has a correct 'data-sorted' attribute." + ); + const sorted = + direction == "ascending" + ? L10N.getStr("networkMenu.sortedAsc") + : L10N.getStr("networkMenu.sortedDesc"); + ok( + header.getAttribute("title").includes(sorted), + "The " + + header.id + + " header includes the used sorting in the 'title' attribute." + ); + } + } + } + + async function testContents([a, b, c, d, e]) { + isnot( + getSelectedRequest(store.getState()), + undefined, + "There should still be a selected item after sorting." + ); + is( + getSelectedIndex(store.getState()), + a, + "The first item should be still selected after sorting." + ); + is( + !!document.querySelector(".network-details-bar"), + true, + "The network details panel should still be visible after sorting." + ); + + is( + getSortedRequests(store.getState()).length, + 5, + "There should be a total of 5 items in the requests menu." + ); + is( + getDisplayedRequests(store.getState()).length, + 5, + "There should be a total of 5 visible items in the requests menu." + ); + is( + document.querySelectorAll(".request-list-item").length, + 5, + "The visible items in the requests menu are, in fact, visible!" + ); + + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + } + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[a], + "GET1", + SORTING_SJS + "?index=1", + { + fuzzyUrl: true, + status: 101, + statusText: "Meh", + type: "1", + fullMimeType: "text/1", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 198), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0), + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[b], + "GET2", + SORTING_SJS + "?index=2", + { + fuzzyUrl: true, + status: 200, + statusText: "Meh", + type: "2", + fullMimeType: "text/2", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 217), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 19), + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[c], + "GET3", + SORTING_SJS + "?index=3", + { + fuzzyUrl: true, + status: 300, + statusText: "Meh", + type: "3", + fullMimeType: "text/3", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 227), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 29), + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[d], + "GET4", + SORTING_SJS + "?index=4", + { + fuzzyUrl: true, + status: 400, + statusText: "Meh", + type: "4", + fullMimeType: "text/4", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 237), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 39), + } + ); + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[e], + "GET5", + SORTING_SJS + "?index=5", + { + fuzzyUrl: true, + status: 500, + statusText: "Meh", + type: "5", + fullMimeType: "text/5", + transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 247), + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 49), + } + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_sse-basic.js b/devtools/client/netmonitor/test/browser_net_sse-basic.js new file mode 100644 index 0000000000..858ce40053 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_sse-basic.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test basic SSE connection. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + "http://mochi.test:8888/browser/devtools/client/netmonitor/test/html_sse-test-page.html", + { + requestCount: 1, + } + ); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Select the request to open the side panel. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Wait for messages to be displayed in DevTools + const wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 1 + ); + + // Test that 'Save Response As' is not in the context menu + EventUtils.sendMouseEvent({ type: "contextmenu" }, requests[0]); + + ok( + !getContextMenuItem(monitor, "request-list-context-save-response-as"), + "The 'Save Response As' context menu item should be hidden" + ); + + // Close context menu. + const contextMenu = monitor.toolbox.topDoc.querySelector( + 'popupset menupopup[menu-api="true"]' + ); + const popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 1, "There should be one message"); + + is( + frames[0].querySelector(".message-list-payload").textContent, + // Initial whitespace comes from ColumnData. + " Why so serious?", + "Data column shows correct payload" + ); + + // Closed message may already be here + if ( + !document.querySelector("#messages-view .msg-connection-closed-message") + ) { + await waitForDOM( + document, + "#messages-view .msg-connection-closed-message", + 1 + ); + } + + is( + !!document.querySelector("#messages-view .msg-connection-closed-message"), + true, + "Connection closed message should be displayed" + ); + + is( + document.querySelector(".message-network-summary-count").textContent, + "1 message", + "Correct message count is displayed" + ); + + is( + document.querySelector(".message-network-summary-total-size").textContent, + "15 B total", + "Correct total size should be displayed" + ); + + is( + !!document.querySelector(".message-network-summary-total-millis"), + true, + "Total time is displayed" + ); + + is( + document.getElementById("frame-filter-menu"), + null, + "Toolbar filter menu is hidden" + ); + + await waitForTick(); + + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector(".message-list-headers") + ); + + const columns = ["data", "time", "retry", "size", "eventName", "lastEventId"]; + for (const column of columns) { + is( + !!getContextMenuItem(monitor, `message-list-header-${column}-toggle`), + true, + `Context menu item "${column}" is displayed` + ); + } + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_stacktraces-visibility.js b/devtools/client/netmonitor/test/browser_net_stacktraces-visibility.js new file mode 100644 index 0000000000..2edb68c4c2 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_stacktraces-visibility.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that opening the stacktrace details panel in the netmonitor and console + * show the expected stacktraces. + */ + +add_task(async function () { + const URL = EXAMPLE_URL + "html_single-get-page.html"; + const REQUEST = + "http://example.com/browser/devtools/client/netmonitor/test/request_0"; + + const { monitor } = await initNetMonitor(URL, { + requestCount: 1, + }); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + info("Starting test... "); + + const allRequestsVisible = waitUntil( + () => document.querySelectorAll(".request-list-item").length == 2 + ); + + await waitForAllNetworkUpdateEvents(); + await reloadBrowser(); + await allRequestsVisible; + + const onStackTracesVisible = waitUntil( + () => document.querySelector("#stack-trace-panel .stack-trace .frame-link"), + "Wait for the stacktrace to be rendered" + ); + + // Select the request initiated by html_single-get-page.html + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelector( + `.request-list-item .requests-list-file[title="${REQUEST}"]` + ) + ); + + // Wait for the stack trace tab to show + await waitUntil(() => + document.querySelector(".network-details-bar #stack-trace-tab") + ); + + clickOnSidebarTab(document, "stack-trace"); + + await onStackTracesVisible; + + // Switch to the webconsole. + const { hud } = await monitor.toolbox.selectTool("webconsole"); + await waitFor( + () => + hud.ui.outputNode.querySelector( + ".webconsole-output .cm-s-mozilla.message.network" + ), + "Wait for the network request log to show" + ); + const fetchRequestUrlNode = hud.ui.outputNode.querySelector( + `.webconsole-output .cm-s-mozilla.message.network a[title="${REQUEST}"]` + ); + fetchRequestUrlNode.click(); + + const messageWrapper = fetchRequestUrlNode.closest(".message-body-wrapper"); + + await waitFor( + () => messageWrapper.querySelector(".network-info"), + "Wait for .network-info to be rendered" + ); + + // Select stacktrace details panel and check the content. + messageWrapper.querySelector("#stack-trace-tab").click(); + await waitFor( + () => messageWrapper.querySelector("#stack-trace-panel .frame-link"), + "Wait for stacktrace to be rendered" + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_statistics-01.js b/devtools/client/netmonitor/test/browser_net_statistics-01.js new file mode 100644 index 0000000000..5a0c5f661e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_statistics-01.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the statistics panel displays correctly. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(STATISTICS_URL, { requestCount: 1 }); + info("Starting test... "); + + const panel = monitor.panelWin; + const { document, store, windowRequire, connector } = panel; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + ok( + document.querySelector(".monitor-panel"), + "The current main panel is correct." + ); + + info("Displaying statistics panel"); + store.dispatch(Actions.openStatistics(connector, true)); + + ok( + document.querySelector(".statistics-panel"), + "The current main panel is correct." + ); + + info("Waiting for placeholder to display"); + + await waitUntil( + () => + document.querySelectorAll(".pie-chart-container[placeholder=true]") + .length == 2 + ); + ok(true, "Two placeholder pie charts appear to be rendered correctly."); + + await waitUntil( + () => + document.querySelectorAll(".table-chart-container[placeholder=true]") + .length == 2 + ); + ok(true, "Two placeholde table charts appear to be rendered correctly."); + + info("Waiting for chart to display"); + + await waitUntil( + () => + document.querySelectorAll(".pie-chart-container:not([placeholder=true])") + .length == 2 + ); + ok(true, "Two real pie charts appear to be rendered correctly."); + + await waitUntil( + () => + document.querySelectorAll( + ".table-chart-container:not([placeholder=true])" + ).length == 2 + ); + ok(true, "Two real table charts appear to be rendered correctly."); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_statistics-02.js b/devtools/client/netmonitor/test/browser_net_statistics-02.js new file mode 100644 index 0000000000..ccc25a387d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_statistics-02.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test if the correct filtering predicates are used when filtering from + * the performance analysis view. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(FILTERING_URL, { requestCount: 1 }); + info("Starting test... "); + + const panel = monitor.panelWin; + const { document, store, windowRequire, connector } = panel; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-css-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-js-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-ws-button") + ); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-other-button") + ); + testFilterButtonsCustom(monitor, [0, 1, 1, 1, 0, 0, 0, 0, 1, 1]); + info( + "The correct filtering predicates are used before entering perf. analysis mode." + ); + + store.dispatch(Actions.openStatistics(connector, true)); + + ok( + document.querySelector(".statistics-panel"), + "The main panel is switched to the statistics panel." + ); + + await waitUntil( + () => + document.querySelectorAll(".pie-chart-container:not([placeholder=true])") + .length == 2 + ); + ok(true, "Two real pie charts appear to be rendered correctly."); + + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".pie-chart-slice-container") + ); + + ok( + document.querySelector(".monitor-panel"), + "The main panel is switched back to the monitor panel." + ); + + testFilterButtons(monitor, "html"); + info( + "The correct filtering predicate is used when exiting perf. analysis mode." + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_statistics-edge-case.js b/devtools/client/netmonitor/test/browser_net_statistics-edge-case.js new file mode 100644 index 0000000000..811147743b --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_statistics-edge-case.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if the statistics panel displays correctly for a page containing + * requests which have been known to create issues in the past for this Panel: + * - cached image (http-on-image-cache-response) requests in the content process + * - long polling requests remaining open for a long time + */ + +add_task(async function () { + // We start the netmonitor on a basic page to avoid opening the panel on + // an incomplete polling request. + const { monitor } = await initNetMonitor(SIMPLE_URL, { + enableCache: true, + }); + + // The navigation should lead to 3 requests (html + image + long polling). + // Additionally we will have a 4th request to call unblock(), so there are 4 + // requests in total. + // However we need to make sure that unblock() is only called once the long + // polling request has been started, so we wait for network events in 2 sets: + // - first the 3 initial requests, with only 2 completing + // - then the unblock request, bundled with the completion of the long polling + let onNetworkEvents = waitForNetworkEvents(monitor, 3, { + expectedPayloadReady: 2, + expectedEventTimings: 2, + }); + + // Here we explicitly do not await on navigateTo. + // The netmonitor will not emit "reloaded" if there are pending requests, + // so if the long polling request already started, we will never receive the + // event. Waiting for the network events should be sufficient here. + const onNavigationCompleted = navigateTo(STATISTICS_EDGE_CASE_URL); + + info("Wait for the 3 first network events (initial)"); + await onNetworkEvents; + + // Prepare to wait for the second set of network events. + onNetworkEvents = waitForNetworkEvents(monitor, 1, { + expectedPayloadReady: 2, + expectedEventTimings: 2, + }); + + // Calling unblock() should allow for the polling request to be displayed and + // to complete, so we can consistently expect 2 events and 2 timings. + info("Call unblock()"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.wrappedJSObject.unblock() + ); + + info("Wait for polling and unblock (initial)"); + await onNetworkEvents; + + info("Wait for the navigation to complete"); + await onNavigationCompleted; + + // Opening the statistics panel will trigger a reload, expect the same requests + // again, we use the same pattern to wait for network events. + onNetworkEvents = waitForNetworkEvents(monitor, 3, { + expectedPayloadReady: 2, + expectedEventTimings: 2, + }); + + info("Open the statistics panel"); + const panel = monitor.panelWin; + const { document, store, windowRequire, connector } = panel; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.openStatistics(connector, true)); + + await waitFor( + () => !!document.querySelector(".statistics-panel"), + "The statistics panel is displayed" + ); + + await waitFor( + () => + document.querySelectorAll( + ".table-chart-container:not([placeholder=true])" + ).length == 2, + "Two real table charts appear to be rendered correctly." + ); + + info("Close statistics panel"); + store.dispatch(Actions.openStatistics(connector, false)); + + await waitFor( + () => !!document.querySelector(".monitor-panel"), + "The regular netmonitor panel is displayed" + ); + info("Wait for the 3 first network events (after opening statistics panel)"); + await onNetworkEvents; + + onNetworkEvents = waitForNetworkEvents(monitor, 1, { + expectedPayloadReady: 2, + expectedEventTimings: 2, + }); + + info("Call unblock()"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => + content.wrappedJSObject.unblock() + ); + + // We need to cleanly wait for all events to be finished, otherwise the UI + // will throw many unhandled promise rejections. + info("Wait for polling and unblock (after opening statistics panel)"); + await onNetworkEvents; + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_status-bar-transferred-size.js b/devtools/client/netmonitor/test/browser_net_status-bar-transferred-size.js new file mode 100644 index 0000000000..4165ffbcd9 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_status-bar-transferred-size.js @@ -0,0 +1,153 @@ +"use strict"; + +/** + * Test if the value of total data transferred is displayed correctly in the Status Bar + * Test for Bug 1481002 + */ +add_task(async function testTotalTransferredSize() { + // Clear cache, so we see expected number of cached requests. + Services.cache2.clear(); + // Disable rcwn to make cache behavior deterministic. + await pushPref("network.http.rcwn.enabled", false); + + const { + getFormattedSize, + } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); + + const { tab, monitor } = await initNetMonitor(STATUS_CODES_URL, { + enableCache: true, + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequestsSummary } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const { L10N } = windowRequire("devtools/client/netmonitor/src/utils/l10n"); + + store.dispatch(Actions.batchEnable(false)); + + info("Performing requests..."); + const onNetworkEvents = waitForNetworkEvents(monitor, 2); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.wrappedJSObject.performOneCachedRequest(); + }); + info("Wait until we get network events for cached request "); + await onNetworkEvents; + + let cachedItemsInUI = 0; + for (const requestItem of document.querySelectorAll(".request-list-item")) { + const requestTransferStatus = requestItem.querySelector( + ".requests-list-transferred" + ).textContent; + if (requestTransferStatus === "cached") { + cachedItemsInUI++; + } + } + + is(cachedItemsInUI, 1, "Number of cached requests displayed is correct"); + + const state = store.getState(); + const totalRequestsCount = state.requests.requests.length; + const requestsSummary = getDisplayedRequestsSummary(state); + info(`Current requests: ${requestsSummary.count} of ${totalRequestsCount}.`); + + const valueTransfer = document.querySelector( + ".requests-list-network-summary-transfer" + ).textContent; + info("Current summary transfer: " + valueTransfer); + const expectedTransfer = L10N.getFormatStrWithNumbers( + "networkMenu.summary.transferred", + getFormattedSize(requestsSummary.contentSize), + getFormattedSize(requestsSummary.transferredSize) + ); + + is( + valueTransfer, + expectedTransfer, + "The current summary transfer is correct." + ); + + await teardown(monitor); +}); + +// Tests the size for the service worker requests are not included as part +// of the total transferred size. +add_task(async function testTotalTransferredSizeWithServiceWorkerRequests() { + // Service workers only work on https + const TEST_URL = HTTPS_EXAMPLE_URL + "service-workers/status-codes.html"; + const { tab, monitor } = await initNetMonitor(TEST_URL, { + enableCache: true, + requestCount: 1, + }); + info("Starting test... "); + + const { store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequestsSummary, getDisplayedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + info("Performing requests before service worker..."); + await performRequests(monitor, tab, 1); + + info("Registering the service worker..."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.registerServiceWorker(); + }); + + info("Performing requests which are intercepted by service worker..."); + await performRequests(monitor, tab, 1); + + let displayedServiceWorkerRequests = 0; + //let totalRequestsTransferredSize = 0; + let totalRequestsTransferredSizeWithoutServiceWorkers = 0; + + const displayedRequests = getDisplayedRequests(store.getState()); + + for (const request of displayedRequests) { + if (request.fromServiceWorker === true) { + displayedServiceWorkerRequests++; + } else { + totalRequestsTransferredSizeWithoutServiceWorkers += + request.transferredSize; + } + //totalRequestsTransferredSize += request.transferredSize; + } + + is( + displayedServiceWorkerRequests, + 4, + "Number of service worker requests displayed is correct" + ); + + /* + NOTE: Currently only intercepted service worker requests are displayed and the transferred times for these are + mostly always zero. Once Bug 1432311 (for fetch requests from the service worker) gets fixed, there should be requests with + > 0 transfered times, allowing to assert this properly. + isnot( + totalRequestsTransferredSize, + totalRequestsTransferredSizeWithoutServiceWorkers, + "The total transferred size including service worker requests is not equal to the total transferred size excluding service worker requests" + ); + */ + + const requestsSummary = getDisplayedRequestsSummary(store.getState()); + + is( + totalRequestsTransferredSizeWithoutServiceWorkers, + requestsSummary.transferredSize, + "The current total transferred size is correct." + ); + + info("Unregistering the service worker..."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.wrappedJSObject.unregisterServiceWorker(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_status-bar.js b/devtools/client/netmonitor/test/browser_net_status-bar.js new file mode 100644 index 0000000000..3ca8ac2da6 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_status-bar.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test whether the StatusBar properly renders expected labels. + */ +add_task(async () => { + const { monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.reduceTimerPrecision", false]], + }); + + const requestsDone = waitForNetworkEvents(monitor, 1); + const markersDone = waitForTimelineMarkers(monitor); + await reloadBrowser(); + await Promise.all([requestsDone, markersDone]); + + const statusBar = document.querySelector(".devtools-toolbar-bottom"); + const requestCount = statusBar.querySelector( + ".requests-list-network-summary-count" + ); + const size = statusBar.querySelector( + ".requests-list-network-summary-transfer" + ); + const onContentLoad = statusBar.querySelector(".dom-content-loaded"); + const onLoad = statusBar.querySelector(".load"); + + // All expected labels should be there + ok(requestCount, "There must be request count label"); + ok(size, "There must be size label"); + ok(onContentLoad, "There must be DOMContentLoaded label"); + ok(onLoad, "There must be load label"); + + // The content should not be empty. The UI update can also be async, + // so use waitUntil. + await waitUntil(() => requestCount.textContent); + ok(true, "There must be request count label text"); + + await waitUntil(() => size.textContent); + ok(true, "There must be size label text"); + + await waitUntil(() => onContentLoad.textContent); + ok(true, "There must be DOMContentLoaded label text"); + + await waitUntil(() => onLoad.textContent); + ok(true, "There must be load label text"); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_status-codes.js b/devtools/client/netmonitor/test/browser_net_status-codes.js new file mode 100644 index 0000000000..8fd430e740 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_status-codes.js @@ -0,0 +1,213 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if requests display the correct status code and text in the UI. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { tab, monitor } = await initNetMonitor(STATUS_CODES_URL, { + requestCount: 1, + }); + + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const requestItems = []; + + const REQUEST_DATA = [ + { + // request #0 + method: "GET", + uri: STATUS_CODES_SJS + "?sts=100", + correctUri: STATUS_CODES_SJS + "?sts=100", + details: { + status: 101, + statusText: "Switching Protocols", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 0), + time: true, + }, + }, + { + // request #1 + method: "GET", + uri: STATUS_CODES_SJS + "?sts=200", + correctUri: STATUS_CODES_SJS + "?sts=200", + details: { + status: 202, + statusText: "Created", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22), + time: true, + }, + }, + { + // request #2 + method: "GET", + uri: STATUS_CODES_SJS + "?sts=300", + correctUri: STATUS_CODES_SJS + "?sts=300", + details: { + status: 303, + statusText: "See Other", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22), + time: true, + }, + }, + { + // request #3 + method: "GET", + uri: STATUS_CODES_SJS + "?sts=400", + correctUri: STATUS_CODES_SJS + "?sts=400", + details: { + status: 404, + statusText: "Not Found", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22), + time: true, + }, + }, + { + // request #4 + method: "GET", + uri: STATUS_CODES_SJS + "?sts=500", + correctUri: STATUS_CODES_SJS + "?sts=500", + details: { + status: 501, + statusText: "Not Implemented", + type: "plain", + fullMimeType: "text/plain; charset=utf-8", + size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 22), + time: true, + }, + }, + ]; + + // Execute requests. + await performRequests(monitor, tab, 5); + + info("Performing tests"); + await verifyRequests(); + await testTab(0, testHeaders); + + return teardown(monitor); + + /** + * A helper that verifies all requests show the correct information and caches + * request list items to requestItems array. + */ + async function verifyRequests() { + const requestListItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestListItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + } + + info("Verifying requests contain correct information."); + let index = 0; + for (const request of REQUEST_DATA) { + const item = getSortedRequests(store.getState())[index]; + requestItems[index] = item; + + info("Verifying request #" + index); + await verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + item, + request.method, + request.uri, + request.details + ); + + index++; + } + } + + /** + * A helper that opens a given tab of request details pane, selects and passes + * all requests to the given test function. + * + * @param Number tabIdx + * The index of tab to activate. + * @param Function testFn(requestItem) + * A function that should perform all necessary tests. It's called once + * for every item of REQUEST_DATA with that item being selected in the + * NetworkMonitor. + */ + async function testTab(tabIdx, testFn) { + let counter = 0; + for (const item of REQUEST_DATA) { + info("Testing tab #" + tabIdx + " to update with request #" + counter); + await testFn(item, counter); + + counter++; + } + } + + /** + * A function that tests "Headers" panel contains correct information. + */ + async function testHeaders(data, index) { + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[index] + ); + + // wait till all the summary section is loaded + await waitUntil(() => + document.querySelector("#headers-panel .tabpanel-summary-value") + ); + const panel = document.querySelector("#headers-panel"); + const { + method, + correctUri, + details: { status, statusText }, + } = data; + + const statusCode = panel.querySelector(".status-code"); + + EventUtils.sendMouseEvent({ type: "mouseover" }, statusCode); + await waitUntil(() => statusCode.title); + + is( + panel.querySelector(".url-preview .url").textContent, + correctUri, + "The url summary value is incorrect." + ); + is( + panel.querySelectorAll(".treeLabel")[0].textContent, + method, + "The method value is incorrect." + ); + is( + parseInt(statusCode.dataset.code, 10), + status, + "The status summary code is incorrect." + ); + is( + statusCode.getAttribute("title"), + status + " " + statusText, + "The status summary value is incorrect." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_streaming-response.js b/devtools/client/netmonitor/test/browser_net_streaming-response.js new file mode 100644 index 0000000000..854c496ca4 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_streaming-response.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if reponses from streaming content types (MPEG-DASH, HLS) are + * displayed as XML or plain text + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + + info("Starting test... "); + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests, getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + const REQUESTS = [ + ["hls-m3u8", /^#EXTM3U/], + ["mpeg-dash", /^<\?xml/], + ]; + + let wait = waitForNetworkEvents(monitor, REQUESTS.length); + for (const [fmt] of REQUESTS) { + const url = CONTENT_TYPE_SJS + "?fmt=" + fmt; + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ url }], + async function (args) { + content.wrappedJSObject.performRequests(1, args.url); + } + ); + } + await wait; + + const requestItems = document.querySelectorAll(".request-list-item"); + for (const requestItem of requestItems) { + requestItem.scrollIntoView(); + const requestsListStatus = requestItem.querySelector(".status-code"); + EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus); + await waitUntil(() => requestsListStatus.title); + await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total"); + } + + REQUESTS.forEach(([fmt], i) => { + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + getSortedRequests(store.getState())[i], + "GET", + CONTENT_TYPE_SJS + "?fmt=" + fmt, + { + status: 200, + statusText: "OK", + } + ); + }); + + wait = waitForDOM(document, "#response-panel"); + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "response"); + await wait; + + store.dispatch(Actions.selectRequest(null)); + + await selectIndexAndWaitForSourceEditor(monitor, 0); + // the hls-m3u8 part + testEditorContent(REQUESTS[0]); + + await selectIndexAndWaitForSourceEditor(monitor, 1); + // the mpeg-dash part + testEditorContent(REQUESTS[1]); + + return teardown(monitor); + + function testEditorContent([fmt, textRe]) { + ok( + getCodeMirrorValue(monitor).match(textRe), + "The text shown in the source editor for " + fmt + " is correct." + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_tabbar_focus.js b/devtools/client/netmonitor/test/browser_net_tabbar_focus.js new file mode 100644 index 0000000000..3bcc7c0ca1 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_tabbar_focus.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if selecting a tab in a tab bar makes it visible + */ +add_task(async function () { + Services.prefs.clearUserPref( + "devtools.netmonitor.panes-network-details-width" + ); + + const { tab, monitor } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const topMostDocument = DevToolsUtils.getTopWindow( + document.defaultView + ).document; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + const networkEvent = waitForNetworkEvents(monitor, 1); + tab.linkedBrowser.reload(); + await networkEvent; + + store.dispatch(Actions.toggleNetworkDetails()); + + const splitter = document.querySelector(".splitter"); + + await EventUtils.synthesizeMouse( + splitter, + 0, + 1, + { type: "mousedown" }, + monitor.panelWin + ); + await EventUtils.synthesizeMouse( + splitter, + 300, + 1, + { type: "mousemove" }, + monitor.panelWin + ); + await EventUtils.synthesizeMouse( + splitter, + 300, + 1, + { type: "mouseup" }, + monitor.panelWin + ); + + await waitUntil(() => document.querySelector(".all-tabs-menu")); + const allTabsMenu = document.querySelector(".all-tabs-menu"); + const panelsWidth = document.querySelector(".tabs-menu").offsetWidth; + + const selectTabFromTabsMenuButton = async id => { + EventUtils.sendMouseEvent({ type: "click" }, allTabsMenu); + const tabMenuElement = topMostDocument.querySelector( + `#devtools-sidebar-${id}` + ); + if (tabMenuElement != null) { + tabMenuElement.click(); + // The tab should be visible within the panel + const tabLi = document.querySelector(`#${id}-tab`).parentElement; + const ulScrollPos = + tabLi.parentElement.scrollLeft + tabLi.parentElement.offsetLeft; + ok( + tabLi.offsetLeft >= ulScrollPos && + tabLi.offsetLeft + tabLi.offsetWidth <= panelsWidth + ulScrollPos, + `The ${id} tab is visible` + ); + } + }; + + for (const elem of [ + "headers", + "cookies", + "request", + "response", + "timings", + "security", + ]) { + await selectTabFromTabsMenuButton(elem); + } + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_telemetry_edit_resend.js b/devtools/client/netmonitor/test/browser_net_telemetry_edit_resend.js new file mode 100644 index 0000000000..3ed6b55aeb --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_telemetry_edit_resend.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +/** + * Test the edit_resend telemetry event. + */ +add_task(async function () { + if ( + Services.prefs.getBoolPref( + "devtools.netmonitor.features.newEditAndResend", + true + ) + ) { + ok( + true, + "Skip this test when pref is true, because this panel won't be default when that is the case." + ); + return; + } + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Remove all telemetry events (you can check about:telemetry). + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + // Reload to have one request in the list. + const waitForEvents = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await waitForEvents; + + // Open context menu and execute "Edit & Resend". + const firstRequest = document.querySelectorAll(".request-list-item")[0]; + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, firstRequest); + await waitForHeaders; + await waitForRequestData(store, ["requestHeaders", "responseHeaders"]); + EventUtils.sendMouseEvent({ type: "contextmenu" }, firstRequest); + + // Open "New Request" form and resend. + await selectContextMenuItem(monitor, "request-list-context-edit-resend"); + await waitUntil(() => document.querySelector("#custom-request-send-button")); + document.querySelector("#custom-request-send-button").click(); + + await waitForNetworkEvents(monitor, 1); + + // Verify existence of the telemetry event. + checkTelemetryEvent( + {}, + { + method: "edit_resend", + } + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_telemetry_filters_changed.js b/devtools/client/netmonitor/test/browser_net_telemetry_filters_changed.js new file mode 100644 index 0000000000..43ae10f489 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_telemetry_filters_changed.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +/** + * Test the filters_changed telemetry event. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + store.dispatch(Actions.batchEnable(false)); + + await waitForAllNetworkUpdateEvents(); + // Remove all telemetry events (you can check about:telemetry). + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + // Reload to have one request in the list. + const wait = waitForNetworkEvents(monitor, 1); + await waitForAllNetworkUpdateEvents(); + await navigateTo(HTTPS_SIMPLE_URL); + await wait; + + info("Click on the 'HTML' filter"); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-html-button") + ); + + checkTelemetryEvent( + { + trigger: "html", + active: "html", + inactive: "all,css,js,xhr,fonts,images,media,ws,other", + }, + { + method: "filters_changed", + } + ); + + info("Click on the 'CSS' filter"); + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector(".requests-list-filter-css-button") + ); + + checkTelemetryEvent( + { + trigger: "css", + active: "html,css", + inactive: "all,js,xhr,fonts,images,media,ws,other", + }, + { + method: "filters_changed", + } + ); + + info("Filter the output using the text filter input"); + setFreetextFilter(monitor, "nomatch"); + + // Wait till the text filter is applied. + await waitUntil(() => !getDisplayedRequests(store.getState()).length); + + checkTelemetryEvent( + { + trigger: "text", + active: "html,css", + inactive: "all,js,xhr,fonts,images,media,ws,other", + }, + { + method: "filters_changed", + } + ); + + return teardown(monitor); +}); + +function setFreetextFilter(monitor, value) { + const { document } = monitor.panelWin; + + const filterBox = document.querySelector(".devtools-filterinput"); + filterBox.focus(); + filterBox.value = ""; + typeInNetmonitor(value, monitor); +} diff --git a/devtools/client/netmonitor/test/browser_net_telemetry_persist_toggle_changed.js b/devtools/client/netmonitor/test/browser_net_telemetry_persist_toggle_changed.js new file mode 100644 index 0000000000..7a8aa956d4 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_telemetry_persist_toggle_changed.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the log persistence telemetry event + */ +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +function togglePersistLogsOption(monitor) { + clickSettingsMenuItem(monitor, "persist-logs"); +} + +function ensurePersistLogsCheckedState(monitor, isChecked) { + openSettingsMenu(monitor); + const persistNode = getSettingsMenuItem(monitor, "persist-logs"); + return !!persistNode?.getAttribute("aria-checked") === isChecked; +} + +add_task(async function () { + const { monitor } = await initNetMonitor(SINGLE_GET_URL, { requestCount: 1 }); + info("Starting test... "); + + const { store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + await waitForAllNetworkUpdateEvents(); + + // Clear all events + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + TelemetryTestUtils.assertNumberOfEvents(0); + + // Click on the toggle - "true" and make sure it was set to correct value + let onPersistChanged = monitor.panelWin.api.once(TEST_EVENTS.PERSIST_CHANGED); + togglePersistLogsOption(monitor); + await waitUntil(() => ensurePersistLogsCheckedState(monitor, true)); + await onPersistChanged; + + // Click a second time - "false" and make sure it was set to correct value + onPersistChanged = monitor.panelWin.api.once(TEST_EVENTS.PERSIST_CHANGED); + togglePersistLogsOption(monitor); + await waitUntil(() => ensurePersistLogsCheckedState(monitor, false)); + await onPersistChanged; + + const expectedEvents = [ + { + category: "devtools.main", + method: "persist_changed", + object: "netmonitor", + value: "true", + }, + { + category: "devtools.main", + method: "persist_changed", + object: "netmonitor", + value: "false", + }, + ]; + + const filter = { + category: "devtools.main", + method: "persist_changed", + object: "netmonitor", + }; + + await waitForAllNetworkUpdateEvents(); + // Will compare filtered events to event list above + await TelemetryTestUtils.assertEvents(expectedEvents, filter); + + // Set Persist log preference back to false + Services.prefs.setBoolPref("devtools.netmonitor.persistlog", false); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_telemetry_select_ws_frame.js b/devtools/client/netmonitor/test/browser_net_telemetry_select_ws_frame.js new file mode 100644 index 0000000000..c3a55dc8cf --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_telemetry_select_ws_frame.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the select_ws_frame telemetry event. + */ +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Clear all events. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged. + TelemetryTestUtils.assertNumberOfEvents(0); + + // Wait for WS connection to be established + send messages. + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(1); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Select the request to open the side panel. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Wait for all sent/received messages to be displayed in DevTools. + const wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Click on the "Response" panel. + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel. + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results. + is(frames.length, 2, "There should be two frames"); + + // Wait for tick, so the `mousedown` event works. + await waitForTick(); + + // Wait for the payload to be resolved (LongString) + const payloadResolved = monitor.panelWin.api.once( + TEST_EVENTS.LONGSTRING_RESOLVED + ); + + // Select frame + EventUtils.sendMouseEvent({ type: "mousedown" }, frames[0]); + await payloadResolved; + + // Verify existence of the telemetry event. + checkTelemetryEvent( + {}, + { + method: "select_ws_frame", + } + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_telemetry_sidepanel_changed.js b/devtools/client/netmonitor/test/browser_net_telemetry_sidepanel_changed.js new file mode 100644 index 0000000000..4cb482e569 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_telemetry_sidepanel_changed.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +/** + * Test the sidepanel_changed telemetry event. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Remove all telemetry events (you can check about:telemetry). + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + // Reload to have one request in the list. + const waitForEvents = waitForNetworkEvents(monitor, 1); + await navigateTo(HTTPS_SIMPLE_URL); + await waitForEvents; + + // Click on a request and wait till the default "Headers" side panel is opened. + info("Click on a request"); + const waitForHeaders = waitUntil(() => + document.querySelector(".headers-overview") + ); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await waitForHeaders; + await waitForRequestData(store, ["requestHeaders", "responseHeaders"]); + + // Click on the Cookies panel and wait till it's opened. + info("Click on the Cookies panel"); + clickOnSidebarTab(document, "cookies"); + await waitForRequestData(store, ["requestCookies", "responseCookies"]); + + checkTelemetryEvent( + { + oldpanel: "headers", + newpanel: "cookies", + }, + { + method: "sidepanel_changed", + } + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_telemetry_throttle_changed.js b/devtools/client/netmonitor/test/browser_net_telemetry_throttle_changed.js new file mode 100644 index 0000000000..ee27f5de2d --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_telemetry_throttle_changed.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + +/** + * Test the throttle_change telemetry event. + */ +add_task(async function () { + const { monitor, toolbox } = await initNetMonitor(SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Remove all telemetry events. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + ok(!snapshot.parent, "No events have been logged for the main process"); + + document.getElementById("network-throttling-menu").click(); + // Throttling menu items cannot be retrieved by id so we can't use getContextMenuItem + // here. Instead use querySelector on the toolbox top document, where the context menu + // will be rendered. + const item = toolbox.topWindow.document.querySelector( + "menuitem[label='GPRS']" + ); + await BrowserTestUtils.waitForPopupEvent(item.parentNode, "shown"); + item.parentNode.activateItem(item); + await monitor.panelWin.api.once(TEST_EVENTS.THROTTLING_CHANGED); + + // Verify existence of the telemetry event. + checkTelemetryEvent( + { + mode: "GPRS", + }, + { + method: "throttle_changed", + } + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_throttle.js b/devtools/client/netmonitor/test/browser_net_throttle.js new file mode 100644 index 0000000000..dcdfde3ec7 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_throttle.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Network throttling integration test. + +"use strict"; + +requestLongerTimeout(2); + +add_task(async function () { + await throttleTest({ throttle: true, addLatency: true }); + await throttleTest({ throttle: true, addLatency: false }); + await throttleTest({ throttle: false, addLatency: false }); +}); + +async function throttleTest(options) { + const { throttle, addLatency } = options; + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + const { store, windowRequire, connector } = monitor.panelWin; + const { ACTIVITY_TYPE } = windowRequire( + "devtools/client/netmonitor/src/constants" + ); + const { updateNetworkThrottling, triggerActivity } = connector; + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + info(`Starting test... (throttle = ${throttle}, addLatency = ${addLatency})`); + + // When throttling, must be smaller than the length of the content + // of SIMPLE_URL in bytes. + const size = throttle ? 200 : 0; + const latency = addLatency ? 100 : 0; + + const throttleProfile = { + latency, + download: size, + upload: 10000, + }; + + info("sending throttle request"); + + await updateNetworkThrottling(true, throttleProfile); + + const wait = waitForNetworkEvents(monitor, 1); + await triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED); + await wait; + + await waitForRequestData(store, ["eventTimings"]); + + const requestItem = getSortedRequests(store.getState())[0]; + const reportedOneSecond = requestItem.eventTimings.timings.receive > 1000; + if (throttle) { + ok(reportedOneSecond, "download reported as taking more than one second"); + } else { + ok(!reportedOneSecond, "download reported as taking less than one second"); + } + + await teardown(monitor); +} diff --git a/devtools/client/netmonitor/test/browser_net_throttling_profiles.js b/devtools/client/netmonitor/test/browser_net_throttling_profiles.js new file mode 100644 index 0000000000..fe5f2d6cb7 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_throttling_profiles.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test all the network throttling profiles + +"use strict"; + +requestLongerTimeout(2); + +const { + profiles, +} = require("resource://devtools/client/shared/components/throttling/profiles.js"); + +const httpServer = createTestHTTPServer(); +httpServer.registerPathHandler(`/`, function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(`<meta charset=utf8><h1>Test throttling profiles</h1>`); +}); + +// The "data" path takes a size query parameter and will return a body of the +// requested size. +httpServer.registerPathHandler("/data", function (request, response) { + const size = request.queryString.match(/size=(\d+)/)[1]; + response.setHeader("Content-Type", "text/plain"); + + response.setStatusLine(request.httpVersion, 200, "OK"); + const body = new Array(size * 1).join("a"); + response.bodyOutputStream.write(body, body.length); +}); + +const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`; + +add_task(async function () { + await pushPref("devtools.cache.disabled", true); + + const { monitor } = await initNetMonitor(TEST_URI, { requestCount: 1 }); + const { store, connector, windowRequire } = monitor.panelWin; + const { updateNetworkThrottling } = connector; + + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + for (const profile of profiles) { + info(`Starting test for throttling profile ${JSON.stringify(profile)}`); + + info("sending throttle request"); + await updateNetworkThrottling(true, profile); + + const onRequest = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [profile], _profile => { + // Size must be greater than the profile download cap. + const size = _profile.download * 2; + content.fetch("data?size=" + size); + }); + await onRequest; + + info(`Wait for eventTimings for throttling profile ${profile.id}`); + await waitForRequestData(store, ["eventTimings"]); + + const requestItem = getSortedRequests(store.getState()).at(-1); + if (requestItem.eventTimings) { + Assert.greater( + requestItem.eventTimings.timings.receive, + 1000, + `Request was properly throttled for profile ${profile.id}` + ); + } + } + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_timeline_ticks.js b/devtools/client/netmonitor/test/browser_net_timeline_ticks.js new file mode 100644 index 0000000000..f25bad87c4 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_timeline_ticks.js @@ -0,0 +1,209 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if timeline correctly displays interval divisions. + */ + +add_task(async function () { + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { monitor } = await initNetMonitor(HTTPS_SIMPLE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { $, $all, NetMonitorView, NetMonitorController } = monitor.panelWin; + const { RequestsMenu } = NetMonitorView; + + // Disable transferred size column support for this test. + // Without this, the waterfall only has enough room for one division, which + // would remove most of the value of this test. + // $("#requests-list-transferred-header-box").hidden = true; + // $("#requests-list-item-template .requests-list-transferred").hidden = true; + + RequestsMenu.lazyUpdate = false; + + ok( + $("#requests-list-waterfall-label"), + "An timeline label should be displayed when the frontend is opened." + ); + ok( + !$all(".requests-list-timings-division").length, + "No tick labels should be displayed when the frontend is opened." + ); + + ok( + !RequestsMenu._canvas, + "No canvas should be created when the frontend is opened." + ); + ok( + !RequestsMenu._ctx, + "No 2d context should be created when the frontend is opened." + ); + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + // Make sure the DOMContentLoaded and load markers don't interfere with + // this test by removing them and redrawing the waterfall (bug 1224088). + NetMonitorController.NetworkEventsHandler.clearMarkers(); + RequestsMenu._flushWaterfallViews(true); + + ok( + !$("#requests-list-waterfall-label"), + "The timeline label should be hidden after the first request." + ); + Assert.greaterOrEqual( + $all(".requests-list-timings-division").length, + 3, + "There should be at least 3 tick labels in the network requests header." + ); + + const timingDivisionEls = $all(".requests-list-timings-division"); + is( + timingDivisionEls[0].textContent, + L10N.getFormatStr("networkMenu.millisecond", 0), + "The first tick label has correct value" + ); + is( + timingDivisionEls[1].textContent, + L10N.getFormatStr("networkMenu.millisecond", 80), + "The second tick label has correct value" + ); + is( + timingDivisionEls[2].textContent, + L10N.getFormatStr("networkMenu.millisecond", 160), + "The third tick label has correct value" + ); + + is( + timingDivisionEls[0].style.width, + "78px", + "The first tick label has correct width" + ); + is( + timingDivisionEls[1].style.width, + "80px", + "The second tick label has correct width" + ); + is( + timingDivisionEls[2].style.width, + "80px", + "The third tick label has correct width" + ); + + ok( + RequestsMenu._canvas, + "A canvas should be created after the first request." + ); + ok( + RequestsMenu._ctx, + "A 2d context should be created after the first request." + ); + + const imageData = RequestsMenu._ctx.getImageData(0, 0, 161, 1); + ok(imageData, "The image data should have been created."); + + const { data } = imageData; + ok(data, "The image data should contain a pixel array."); + + ok(hasPixelAt(0), "The tick at 0 is should not be empty."); + ok(!hasPixelAt(1), "The tick at 1 is should be empty."); + ok(!hasPixelAt(19), "The tick at 19 is should be empty."); + ok(hasPixelAt(20), "The tick at 20 is should not be empty."); + ok(!hasPixelAt(21), "The tick at 21 is should be empty."); + ok(!hasPixelAt(39), "The tick at 39 is should be empty."); + ok(hasPixelAt(40), "The tick at 40 is should not be empty."); + ok(!hasPixelAt(41), "The tick at 41 is should be empty."); + ok(!hasPixelAt(59), "The tick at 59 is should be empty."); + ok(hasPixelAt(60), "The tick at 60 is should not be empty."); + ok(!hasPixelAt(61), "The tick at 61 is should be empty."); + ok(!hasPixelAt(79), "The tick at 79 is should be empty."); + ok(hasPixelAt(80), "The tick at 80 is should not be empty."); + ok(!hasPixelAt(81), "The tick at 81 is should be empty."); + ok(!hasPixelAt(159), "The tick at 159 is should be empty."); + ok(hasPixelAt(160), "The tick at 160 is should not be empty."); + ok(!hasPixelAt(161), "The tick at 161 is should be empty."); + + ok( + isPixelBrighterAtThan(0, 20), + "The tick at 0 should be brighter than the one at 20" + ); + ok( + isPixelBrighterAtThan(40, 20), + "The tick at 40 should be brighter than the one at 20" + ); + ok( + isPixelBrighterAtThan(40, 60), + "The tick at 40 should be brighter than the one at 60" + ); + ok( + isPixelBrighterAtThan(80, 60), + "The tick at 80 should be brighter than the one at 60" + ); + + ok( + isPixelBrighterAtThan(80, 100), + "The tick at 80 should be brighter than the one at 100" + ); + ok( + isPixelBrighterAtThan(120, 100), + "The tick at 120 should be brighter than the one at 100" + ); + ok( + isPixelBrighterAtThan(120, 140), + "The tick at 120 should be brighter than the one at 140" + ); + ok( + isPixelBrighterAtThan(160, 140), + "The tick at 160 should be brighter than the one at 140" + ); + + ok( + isPixelEquallyBright(20, 60), + "The tick at 20 should be equally bright to the one at 60" + ); + ok( + isPixelEquallyBright(100, 140), + "The tick at 100 should be equally bright to the one at 140" + ); + + ok( + isPixelEquallyBright(40, 120), + "The tick at 40 should be equally bright to the one at 120" + ); + + ok( + isPixelEquallyBright(0, 80), + "The tick at 80 should be equally bright to the one at 160" + ); + ok( + isPixelEquallyBright(80, 160), + "The tick at 80 should be equally bright to the one at 160" + ); + + function hasPixelAt(x) { + const i = (x | 0) * 4; + return data[i] && data[i + 1] && data[i + 2] && data[i + 3]; + } + + function isPixelBrighterAtThan(x1, x2) { + const i = (x1 | 0) * 4; + const j = (x2 | 0) * 4; + return data[i + 3] > data[j + 3]; + } + + function isPixelEquallyBright(x1, x2) { + const i = (x1 | 0) * 4; + const j = (x2 | 0) * 4; + return data[i + 3] == data[j + 3]; + } + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_timing-division.js b/devtools/client/netmonitor/test/browser_net_timing-division.js new file mode 100644 index 0000000000..05935999a1 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_timing-division.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if timing intervals are divided against seconds when appropriate. + */ +add_task(async function () { + // Show only few columns, so there is enough space + // for the waterfall. + await pushPref( + "devtools.netmonitor.visibleColumns", + '["status", "contentSize", "waterfall"]' + ); + + const { tab, monitor } = await initNetMonitor(CUSTOM_GET_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 2); + // Timeout needed for having enough divisions on the time scale. + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.wrappedJSObject.performRequests(2, null, 3000); + }); + await wait; + + const milDivs = document.querySelectorAll( + ".requests-list-timings-division[data-division-scale=millisecond]" + ); + const secDivs = document.querySelectorAll( + ".requests-list-timings-division[data-division-scale=second]" + ); + const minDivs = document.querySelectorAll( + ".requests-list-timings-division[data-division-scale=minute]" + ); + + info("Number of millisecond divisions: " + milDivs.length); + info("Number of second divisions: " + secDivs.length); + info("Number of minute divisions: " + minDivs.length); + + milDivs.forEach(div => info(`Millisecond division: ${div.textContent}`)); + secDivs.forEach(div => info(`Second division: ${div.textContent}`)); + minDivs.forEach(div => info(`Minute division: ${div.textContent}`)); + + is( + store.getState().requests.requests.length, + 2, + "There should be only two requests made." + ); + + ok( + secDivs.length, + "There should be at least one division on the seconds time scale." + ); + ok( + secDivs[0].textContent.match(/\d+\.\d{2}\s\w+/), + "The division on the seconds time scale looks legit." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_tracking-resources.js b/devtools/client/netmonitor/test/browser_net_tracking-resources.js new file mode 100644 index 0000000000..6d7365cb73 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_tracking-resources.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); + +const TEST_URI = + "https://example.com/browser/devtools/client/" + + "netmonitor/test/html_tracking-protection.html"; + +registerCleanupFunction(function () { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +/** + * Test that tracking resources are properly marked in the Network panel. + */ +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + const { monitor, tab } = await initNetMonitor(TEST_URI, { requestCount: 1 }); + info("Starting test..."); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute request with third party tracking protection flag. + await performRequests(monitor, tab, 1); + + const domainRequests = document.querySelectorAll( + ".requests-list-domain .tracking-resource" + ); + const UrlRequests = document.querySelectorAll( + ".requests-list-url .tracking-resource" + ); + + is( + domainRequests.length, + 1, + "There should be one domain column tracking request" + ); + is(UrlRequests.length, 1, "There should be one URL column tracking request"); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_truncate-post-data.js b/devtools/client/netmonitor/test/browser_net_truncate-post-data.js new file mode 100644 index 0000000000..70cdab11b4 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_truncate-post-data.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1542172 - + * Verifies that requests with large post data are truncated and error is displayed. + */ +add_task(async function () { + const { monitor, tab } = await initNetMonitor(POST_JSON_URL, { + requestCount: 1, + }); + + info("Starting test... "); + + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + requestLongerTimeout(2); + + info("Perform requests"); + await performRequestsAndWait(monitor, tab); + + await waitUntil(() => document.querySelector(".request-list-item")); + const item = document.querySelectorAll(".request-list-item")[0]; + await waitUntil(() => item.querySelector(".requests-list-type").title); + + // Make sure the header and editor are loaded + const waitHeader = waitForDOM(document, "#request-panel .data-header"); + const waitSourceEditor = waitForDOM( + document, + "#request-panel .CodeMirror.cm-s-mozilla" + ); + + store.dispatch(Actions.toggleNetworkDetails()); + clickOnSidebarTab(document, "request"); + + await Promise.all([waitHeader, waitSourceEditor]); + + const tabpanel = document.querySelector("#request-panel"); + is( + tabpanel.querySelector(".request-error-header") === null, + false, + "The request error header doesn't have the intended visibility." + ); + is( + tabpanel.querySelector(".request-error-header").textContent, + "Request has been truncated", + "The error message shown is incorrect" + ); + const jsonView = tabpanel.querySelector(".data-label") || {}; + is( + jsonView.textContent === L10N.getStr("jsonScopeName"), + false, + "The params json view doesn't have the intended visibility." + ); + is( + tabpanel.querySelector("PRE") === null, + false, + "The Request Payload has the intended visibility." + ); + + return teardown(monitor); +}); + +async function performRequestsAndWait(monitor, tab) { + const wait = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + content.wrappedJSObject.performLargePostDataRequest(); + }); + await wait; +} diff --git a/devtools/client/netmonitor/test/browser_net_truncate.js b/devtools/client/netmonitor/test/browser_net_truncate.js new file mode 100644 index 0000000000..77e6451f7e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_truncate.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verifies that truncated response bodies still have the correct reported size. + */ +add_task(async function () { + const limit = Services.prefs.getIntPref( + "devtools.netmonitor.responseBodyLimit" + ); + const URL = EXAMPLE_URL + "sjs_truncate-test-server.sjs?limit=" + limit; + const { monitor } = await initNetMonitor(URL, { requestCount: 1 }); + + info("Starting test... "); + + const { + L10N, + } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + + const { document } = monitor.panelWin; + + const wait = waitForNetworkEvents(monitor, 1); + await reloadBrowser(); + await wait; + + // Response content will be updated asynchronously, we should make sure data is updated + // on DOM before asserting. + await waitUntil(() => document.querySelector(".request-list-item")); + const item = document.querySelectorAll(".request-list-item")[0]; + await waitUntil(() => item.querySelector(".requests-list-type").title); + + const type = item.querySelector(".requests-list-type").textContent; + const fullMimeType = item.querySelector(".requests-list-type").title; + const transferred = item.querySelector( + ".requests-list-transferred" + ).textContent; + const size = item.querySelector(".requests-list-size").textContent; + + is(type, "plain", "Type should be rendered correctly."); + is( + fullMimeType, + "text/plain; charset=utf-8", + "Mimetype should be rendered correctly." + ); + is( + transferred, + L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2.1), + "Transferred size should be rendered correctly." + ); + is( + size, + L10N.getFormatStrWithNumbers("networkMenu.sizeMB", 2.1), + "Size should be rendered correctly." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_url-preview.js b/devtools/client/netmonitor/test/browser_net_url-preview.js new file mode 100644 index 0000000000..ae886671cc --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_url-preview.js @@ -0,0 +1,188 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the url preview expanded state is persisted across requests selections. + */ + +add_task(async function () { + const { monitor, tab } = await initNetMonitor(PARAMS_URL, { + requestCount: 1, + }); + + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 12); + + let wait = waitForDOM(document, "#headers-panel .url-preview", 1); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + await wait; + + // Expand preview + await toggleUrlPreview(true, monitor); + + // Select the second request + wait = waitForDOM(document, "#headers-panel .url-preview", 1); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[1] + ); + await wait; + + // Test that the url is still expanded + const noOfVisibleRowsAfterExpand = document.querySelectorAll( + "#headers-panel .url-preview tr.treeRow" + ).length; + Assert.greater( + noOfVisibleRowsAfterExpand, + 1, + "The url preview should still be expanded." + ); + + // Collapse preview + await toggleUrlPreview(false, monitor); + + // Select the third request + wait = waitForDOM(document, "#headers-panel .url-preview", 1); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[2] + ); + await wait; + + // Test that the url is still collapsed + const noOfVisibleRowsAfterCollapse = document.querySelectorAll( + "#headers-panel .url-preview tr.treeRow" + ).length; + Assert.equal( + noOfVisibleRowsAfterCollapse, + 1, + "The url preview should still be collapsed." + ); + + return teardown(monitor); +}); + +/** + * Checks if the query parameter arrays are formatted as we expected. + */ + +add_task(async function () { + const { monitor } = await initNetMonitor(PARAMS_URL, { + requestCount: 1, + }); + + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const netWorkEvent = waitForNetworkEvents(monitor, 3); + await performRequestsInContent([ + // URL with same parameter name with different values + { url: "sjs_content-type-test-server.sjs?a=3&a=45&a=60" }, + + // URL with mix of different parameter names + { url: "sjs_content-type-test-server.sjs?x=5&a=3&a=4&a=3&b=3" }, + + // URL contains a parameter with `query` as the name. This makes sure + // there is no conflict with the query property on the Url Object in the + // UrlPreview + { url: "sjs_content-type-test-server.sjs?x=5&a=3&a=4&a=3&query=3" }, + + // URL contains a paramter with `__proto__` as the name. This makes sure + // there is no conflict with the prototype chain of the JS object. + { url: "sjs_content-type-test-server.sjs?__proto__=5" }, + ]); + await netWorkEvent; + + let urlPreview = waitForDOM(document, "#headers-panel .url-preview", 1); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[0] + ); + let urlPreviewValue = (await urlPreview)[0].textContent; + + ok( + urlPreviewValue.endsWith("?a=3&a=45&a=60"), + "The parameters in the url preview match." + ); + + urlPreview = waitForDOM(document, "#headers-panel .url-preview", 1); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[1] + ); + + urlPreviewValue = (await urlPreview)[0].textContent; + ok( + urlPreviewValue.endsWith("?x=5&a=3&a=4&a=3&b=3"), + "The parameters in the url preview match." + ); + + urlPreview = waitForDOM(document, "#headers-panel .url-preview", 1); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[2] + ); + + urlPreviewValue = (await urlPreview)[0].textContent; + ok( + urlPreviewValue.endsWith("?x=5&a=3&a=4&a=3&query=3"), + "The parameters in the url preview match." + ); + + // Expand preview + await toggleUrlPreview(true, monitor); + + // Check if the expanded preview contains the "query" parameter + is( + document.querySelector( + "#headers-panel .url-preview tr#\\/GET\\/query\\/query .treeLabelCell" + ).textContent, + "query", + "Contains the query parameter" + ); + + // Collapse preview + await toggleUrlPreview(false, monitor); + + urlPreview = waitForDOM(document, "#headers-panel .url-preview", 1); + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[3] + ); + + urlPreviewValue = (await urlPreview)[0].textContent; + ok( + urlPreviewValue.endsWith("?__proto__=5"), + "The parameters in the url preview match." + ); + + // Expand preview + await toggleUrlPreview(true, monitor); + + // Check if the expanded preview contains the "__proto__" parameter + is( + document.querySelector( + "#headers-panel .url-preview tr#\\/GET\\/query\\/__proto__ .treeLabelCell" + ).textContent, + "__proto__", + "Contains the __proto__ parameter" + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_use_as_fetch.js b/devtools/client/netmonitor/test/browser_net_use_as_fetch.js new file mode 100644 index 0000000000..a49b47f75b --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_use_as_fetch.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests if Use as Fetch works. + */ + +add_task(async function () { + const { tab, monitor, toolbox } = await initNetMonitor(CURL_URL, { + requestCount: 1, + }); + info("Starting test... "); + + // GET request, no cookies (first request) + await performRequest("GET"); + await testConsoleInput(`await fetch("http://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs", { + "credentials": "omit", + "headers": { + "User-Agent": "${navigator.userAgent}", + "Accept": "*/*", + "Accept-Language": "en-US", + "X-Custom-Header-1": "Custom value", + "X-Custom-Header-2": "8.8.8.8", + "X-Custom-Header-3": "Mon, 3 Mar 2014 11:11:11 GMT", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + }, + "referrer": "http://example.com/browser/devtools/client/netmonitor/test/html_copy-as-curl.html", + "method": "GET", + "mode": "cors" +});`); + + await teardown(monitor); + + async function performRequest(method, payload) { + const waitRequest = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + url: SIMPLE_SJS, + method_: method, + payload_: payload, + }, + ], + async function ({ url, method_, payload_ }) { + content.wrappedJSObject.performRequest(url, method_, payload_); + } + ); + await waitRequest; + } + + async function testConsoleInput(expectedResult) { + const { document } = monitor.panelWin; + + const items = document.querySelectorAll(".request-list-item"); + EventUtils.sendMouseEvent({ type: "mousedown" }, items[items.length - 1]); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelectorAll(".request-list-item")[0] + ); + + /* Ensure that the use as fetch option is always visible */ + is( + !!getContextMenuItem(monitor, "request-list-context-use-as-fetch"), + true, + 'The "Use as Fetch" context menu item should not be hidden.' + ); + + const split = toolbox.once("split-console"); + await selectContextMenuItem(monitor, "request-list-context-use-as-fetch"); + + await split; + const hud = toolbox.getPanel("webconsole").hud; + await hud.jsterm.once("set-input-value"); + + is( + hud.getInputValue(), + expectedResult, + "Console input contains fetch request for item " + (items.length - 1) + ); + } +}); diff --git a/devtools/client/netmonitor/test/browser_net_view-source-debugger.js b/devtools/client/netmonitor/test/browser_net_view-source-debugger.js new file mode 100644 index 0000000000..e443001da0 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_view-source-debugger.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// There are shutdown issues for which multiple rejections are left uncaught. +// See bug 1018184 for resolving these issues. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/Component not initialized/); +PromiseTestUtils.allowMatchingRejectionsGlobally(/Connection closed/); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/shared-head.js", + this +); + +/** + * Tests if on clicking the stack frame, UI switches to the Debugger panel. + */ +add_task(async function () { + // Set a higher panel height in order to get full CodeMirror content + await pushPref("devtools.toolbox.footer.height", 400); + + const { tab, monitor, toolbox } = await initNetMonitor(POST_DATA_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + store.dispatch(Actions.batchEnable(false)); + + // Execute requests. + await performRequests(monitor, tab, 2); + + info("Clicking stack-trace tab and waiting for stack-trace panel to open"); + const waitForTab = waitForDOM(document, "#stack-trace-tab"); + // Click on the first request + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelector(".request-list-item") + ); + await waitForTab; + const waitForPanel = waitForDOM( + document, + "#stack-trace-panel .frame-link", + 6 + ); + // Open the stack-trace tab for that request + document.getElementById("stack-trace-tab").click(); + await waitForPanel; + + const frameLinkNode = document.querySelector(".frame-link"); + await checkClickOnNode(toolbox, frameLinkNode); + + await teardown(monitor); +}); + +/** + * Helper function for testOpenInDebugger. + */ +async function checkClickOnNode(toolbox, frameLinkNode) { + info("checking click on node location"); + + const url = frameLinkNode.getAttribute("data-url"); + ok(url, `source url found ("${url}")`); + + const line = frameLinkNode.getAttribute("data-line"); + ok(line, `source line found ("${line}")`); + + // Fire the click event + frameLinkNode.querySelector(".frame-link-source").click(); + + // Wait for the debugger to have fully processed the opened source + await toolbox.getPanelWhenReady("jsdebugger"); + const dbg = createDebuggerContext(toolbox); + await waitForSelectedSource(dbg, url); +} diff --git a/devtools/client/netmonitor/test/browser_net_waterfall-click.js b/devtools/client/netmonitor/test/browser_net_waterfall-click.js new file mode 100644 index 0000000000..a54b3b8eee --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_waterfall-click.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that clicking on the waterfall opens the timing sidebar panel. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + CONTENT_TYPE_WITHOUT_CACHE_URL, + { requestCount: 1 } + ); + const { document } = monitor.panelWin; + + // Execute requests. + await performRequests(monitor, tab, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS); + + info("Clicking waterfall and waiting for panel update."); + const wait = waitForDOM(document, "#timings-panel"); + + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".requests-list-timings")[0] + ); + + await wait; + + ok( + document.querySelector("#timings-tab[aria-selected=true]"), + "Timings tab is selected." + ); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_websocket_stacks.js b/devtools/client/netmonitor/test/browser_net_websocket_stacks.js new file mode 100644 index 0000000000..cb9acf9d5b --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_websocket_stacks.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we get stack traces for the network requests made when creating +// web sockets on the main or worker threads. + +const TOP_FILE_NAME = "html_websocket-test-page.html"; +const TOP_URL = HTTPS_EXAMPLE_URL + TOP_FILE_NAME; +const WORKER_FILE_NAME = "js_websocket-worker-test.js"; + +const EXPECTED_REQUESTS = { + [TOP_URL]: { + method: "GET", + url: TOP_URL, + causeType: "document", + causeUri: null, + stack: false, + }, + "ws://localhost:8080/": { + method: "GET", + url: "ws://localhost:8080/", + causeType: "websocket", + causeUri: TOP_URL, + stack: [ + { fn: "openSocket", file: TOP_URL, line: 6 }, + { file: TOP_FILE_NAME, line: 3 }, + ], + }, + [HTTPS_EXAMPLE_URL + WORKER_FILE_NAME]: { + method: "GET", + url: HTTPS_EXAMPLE_URL + WORKER_FILE_NAME, + causeType: "script", + causeUri: TOP_URL, + stack: [{ file: TOP_URL, line: 9 }], + }, + "wss://localhost:8081/": { + method: "GET", + url: "wss://localhost:8081/", + causeType: "websocket", + causeUri: TOP_URL, + stack: [ + { + fn: "openWorkerSocket", + file: HTTPS_EXAMPLE_URL + WORKER_FILE_NAME, + line: 5, + }, + { file: WORKER_FILE_NAME, line: 2 }, + ], + }, +}; + +add_task(async function () { + // Load a different URL first to instantiate the network monitor before we + // load the page we're really interested in. + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + + const { store, windowRequire, connector } = monitor.panelWin; + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + const onNetworkEvents = waitForNetworkEvents( + monitor, + Object.keys(EXPECTED_REQUESTS).length + ); + await navigateTo(TOP_URL); + await onNetworkEvents; + + is( + store.getState().requests.requests.length, + Object.keys(EXPECTED_REQUESTS).length, + "All the page events should be recorded." + ); + + const requests = getSortedRequests(store.getState()); + // The expected requests in the same order as the + // requests. The platform does not guarantee the order so the + // tests should not depend on that. + const expectedRequests = []; + + // Wait for stack traces from all requests. + await Promise.all( + requests.map(request => { + expectedRequests.push(EXPECTED_REQUESTS[request.url]); + return connector.requestData(request.id, "stackTrace"); + }) + ); + + validateRequests(expectedRequests, monitor); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_worker_stacks.js b/devtools/client/netmonitor/test/browser_net_worker_stacks.js new file mode 100644 index 0000000000..febbea8d09 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_worker_stacks.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we get stack traces for the network requests made when starting or +// running worker threads. + +const TOP_FILE_NAME = "html_worker-test-page.html"; +const TOP_URL = HTTPS_EXAMPLE_URL + TOP_FILE_NAME; +const WORKER_FILE_NAME = "js_worker-test.js"; +const WORKER_URL = HTTPS_EXAMPLE_URL + WORKER_FILE_NAME; + +const EXPECTED_REQUESTS = [ + { + method: "GET", + url: TOP_URL, + causeType: "document", + causeUri: null, + stack: false, + }, + { + method: "GET", + url: WORKER_URL, + causeType: "script", + causeUri: TOP_URL, + stack: [ + { fn: "startWorkerInner", file: TOP_URL, line: 11 }, + { fn: "startWorker", file: TOP_URL, line: 8 }, + { file: TOP_URL, line: 4 }, + ], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "missing1.js", + causeType: "script", + causeUri: TOP_URL, + stack: [ + { fn: "importScriptsFromWorker", file: WORKER_URL, line: 14 }, + { file: WORKER_URL, line: 10 }, + ], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "missing2.js", + causeType: "script", + causeUri: TOP_URL, + stack: [ + { fn: "importScriptsFromWorker", file: WORKER_URL, line: 14 }, + { file: WORKER_URL, line: 10 }, + ], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "js_worker-test2.js", + causeType: "script", + causeUri: TOP_URL, + stack: [ + { fn: "startWorkerFromWorker", file: WORKER_URL, line: 7 }, + { file: WORKER_URL, line: 3 }, + ], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "missing.json", + causeType: "xhr", + causeUri: TOP_URL, + stack: [ + { fn: "createJSONRequest", file: WORKER_URL, line: 22 }, + { file: WORKER_URL, line: 18 }, + ], + }, + { + method: "GET", + url: HTTPS_EXAMPLE_URL + "missing.txt", + causeType: "fetch", + causeUri: TOP_URL, + stack: [ + { fn: "fetchThing", file: WORKER_URL, line: 29 }, + { file: WORKER_URL, line: 26 }, + ], + }, +]; + +add_task(async function () { + // Load a different URL first to instantiate the network monitor before we + // load the page we're really interested in. + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + + const { store, windowRequire, connector } = monitor.panelWin; + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + + const onNetworkEvents = waitForNetworkEvents( + monitor, + EXPECTED_REQUESTS.length + ); + await navigateTo(TOP_URL); + await onNetworkEvents; + + is( + store.getState().requests.requests.length, + EXPECTED_REQUESTS.length, + "All the page events should be recorded." + ); + + // Wait for stack traces from all requests. + const requests = getSortedRequests(store.getState()); + await Promise.all( + requests.map(requestItem => + connector.requestData(requestItem.id, "stackTrace") + ) + ); + + validateRequests(EXPECTED_REQUESTS, monitor, { allowDifferentOrder: true }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-basic.js b/devtools/client/netmonitor/test/browser_net_ws-basic.js new file mode 100644 index 0000000000..a6d0d0070e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-basic.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS connection is established successfully and sent/received messages are correct. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connection to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(1); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Select the request to open the side panel. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Wait for all sent/received messages to be displayed in DevTools + const wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + // Sent frame + is( + frames[0].children[0].textContent, + " Payload 0", + "The correct sent payload should be displayed" + ); + is(frames[0].classList.contains("sent"), true, "The payload type is 'Sent'"); + + // Received frame + is( + frames[1].children[0].textContent, + " Payload 0", + "The correct received payload should be displayed" + ); + is( + frames[1].classList.contains("received"), + true, + "The payload type is 'Received'" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-clear.js b/devtools/client/netmonitor/test/browser_net_ws-clear.js new file mode 100644 index 0000000000..0636f862bd --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-clear.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS connection is established successfully and clearing messages works correctly. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connection(s) to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 2); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(2); + await content.wrappedJSObject.openConnection(1); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 2, "There should be two requests"); + + // Wait for all sent/received messages to be displayed in DevTools + let wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 4 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 4, "There should be four frames"); + + // Clear messages + const clearButton = document.querySelector( + "#messages-view .message-list-clear-button" + ); + clearButton.click(); + is( + document.querySelectorAll(".message-list-empty-notice").length, + 1, + "Empty notice visible" + ); + + // Select the second request and check that the messages are not cleared + wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[1]); + await wait; + const secondRequestFrames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + is(secondRequestFrames.length, 2, "There should be two frames"); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-connection-closed.js b/devtools/client/netmonitor/test/browser_net_ws-connection-closed.js new file mode 100644 index 0000000000..900c11df7e --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-connection-closed.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS connection is established successfully and message is displayed when the connection is closed. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connection to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await ContentTask.spawn(tab.linkedBrowser, {}, async () => { + await content.wrappedJSObject.openConnection(1); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + + // Select the request to open the side panel. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + + const wait = waitForDOM( + document, + "#messages-view .msg-connection-closed-message" + ); + + // Close WS connection + await ContentTask.spawn(tab.linkedBrowser, {}, async () => { + await content.wrappedJSObject.closeConnection(); + }); + await wait; + + is( + !!document.querySelector("#messages-view .msg-connection-closed-message"), + true, + "Connection closed message should be displayed" + ); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-copy-binary-message.js b/devtools/client/netmonitor/test/browser_net_ws-copy-binary-message.js new file mode 100644 index 0000000000..0e72fb1697 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-copy-binary-message.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS binary messages can be copied. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send message + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + const data = { + text: "something", + hex: "736f6d657468696e67", + base64: "c29tZXRoaW5n", + }; + await SpecialPowers.spawn(tab.linkedBrowser, [data.text], async text => { + await content.wrappedJSObject.openConnection(0); + content.wrappedJSObject.sendData(text, true); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + + // Wait for all sent/received messages to be displayed in DevTools + const wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Select the websocket request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Test that 'Save Response As' is not in the context menu + EventUtils.sendMouseEvent({ type: "contextmenu" }, requests[0]); + + ok( + !getContextMenuItem(monitor, "request-list-context-save-response-as"), + "The 'Save Response As' context menu item should be hidden" + ); + + // Close context menu. + const contextMenu = monitor.toolbox.topDoc.querySelector( + 'popupset menupopup[menu-api="true"]' + ); + const popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const messages = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Test all types for both the sent and received message. + for (const message of messages) { + for (const [type, expectedValue] of Object.entries(data)) { + const menuItemId = `message-list-context-copy-message-${type}`; + EventUtils.sendMouseEvent({ type: "contextmenu" }, message); + is( + !!getContextMenuItem(monitor, menuItemId), + true, + `Could not find "${type}" copy option.` + ); + + await waitForClipboardPromise( + async function setup() { + await selectContextMenuItem(monitor, menuItemId); + }, + function validate(result) { + return result === expectedValue; + } + ); + } + } + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-early-connection.js b/devtools/client/netmonitor/test/browser_net_ws-early-connection.js new file mode 100644 index 0000000000..4f27ad9954 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-early-connection.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS connection created while the page is still loading + * is properly tracked and there are WS frames displayed in the + * Messages side panel. + */ +add_task(async function () { + const { monitor } = await initNetMonitor(SIMPLE_URL, { requestCount: 1 }); + + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Make the WS Messages side panel the default so, we avoid + // request headers from the backend by selecting the Headers + // panel + store.dispatch(Actions.selectDetailsPanelTab("response")); + + // Load page that opens WS connection during the load time. + const waitForEvents = waitForNetworkEvents(monitor, 3); + await navigateTo(WS_PAGE_EARLY_CONNECTION_URL); + await waitForEvents; + + const requests = document.querySelectorAll( + ".request-list-item .requests-list-file" + ); + is(requests.length, 3, "There should be three requests"); + + // Get index of the WS connection request. + const index = Array.from(requests).findIndex(element => { + return element.textContent === "file_ws_backend"; + }); + + Assert.notStrictEqual(index, -1, "There must be one WS connection request"); + + // Select the connection request to see WS frames in the side panel. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[index]); + + info("Waiting for WS frames..."); + + // Wait for two frames to be displayed in the panel + await waitForDOMIfNeeded( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Check the payload of the first frame. + const firstFramePayload = document.querySelector( + "#messages-view .message-list-table .message-list-item .message-list-payload" + ); + is(firstFramePayload.textContent.trim(), "readyState:loading"); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-filter-dropdown.js b/devtools/client/netmonitor/test/browser_net_ws-filter-dropdown.js new file mode 100644 index 0000000000..c17fbeba49 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-filter-dropdown.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS connection is established successfully and filtered messages using the dropdown menu are correct. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connection to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 2); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(2); + await content.wrappedJSObject.openConnection(3); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 2, "There should be two requests"); + + // Wait for all sent/received messages to be displayed in DevTools + let wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 4 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 4, "There should be four frames"); + + // Click on filter menu + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#frame-filter-menu") + ); + + // Click on "sent" option and check + await selectContextMenuItem(monitor, "message-list-context-filter-sent"); + + const sentFrames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + is(sentFrames.length, 2, "There should be two frames"); + is( + sentFrames[0].classList.contains("sent"), + true, + "The payload type is 'Sent'" + ); + is( + sentFrames[1].classList.contains("sent"), + true, + "The payload type is 'Sent'" + ); + + // Click on filter menu + EventUtils.sendMouseEvent( + { type: "click" }, + document.querySelector("#frame-filter-menu") + ); + + // Click on "received" option and check + await selectContextMenuItem(monitor, "message-list-context-filter-received"); + + const receivedFrames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + is(receivedFrames.length, 2, "There should be two frames"); + is( + receivedFrames[0].classList.contains("received"), + true, + "The payload type is 'Received'" + ); + is( + receivedFrames[1].classList.contains("received"), + true, + "The payload type is 'Received'" + ); + + // Select the second request and check that the filter option is the same + wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 3 + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[1]); + await wait; + const secondRequestFrames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + is(secondRequestFrames.length, 3, "There should be three frames"); + is( + secondRequestFrames[0].classList.contains("received"), + true, + "The payload type is 'Received'" + ); + is( + secondRequestFrames[1].classList.contains("received"), + true, + "The payload type is 'Received'" + ); + is( + secondRequestFrames[2].classList.contains("received"), + true, + "The payload type is 'Received'" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-filter-regex.js b/devtools/client/netmonitor/test/browser_net_ws-filter-regex.js new file mode 100644 index 0000000000..0081b46cb4 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-filter-regex.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that RegEx filter is worrking. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getDisplayedMessages } = windowRequire( + "devtools/client/netmonitor/src/selectors/messages" + ); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connection(s) to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(1); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for all sent/received messages to be displayed in DevTools + const wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + const filterInput = document.querySelector( + "#messages-view .devtools-filterinput" + ); + filterInput.focus(); + typeInNetmonitor("/Payload [0-9]+/", monitor); + + // Wait till the text filter is applied. + await waitUntil(() => getDisplayedMessages(store.getState()).length == 2); + + const filteredFrames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + is(filteredFrames.length, 2, "There should be two frames"); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-json-action-cable-payload.js b/devtools/client/netmonitor/test/browser_net_ws-json-action-cable-payload.js new file mode 100644 index 0000000000..7ad45a2030 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-json-action-cable-payload.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WebSocket payloads containing action cable messages are parsed + * correctly and displayed in a user friendly way + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(0); + content.wrappedJSObject.sendData(`{"data":"{\\"x\\":2}"}`); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for all sent/received messages to be displayed in DevTools + let wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + await clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + // Wait for next tick to do async stuff (The MessagePayload component uses the async function getMessagePayload) + await waitForTick(); + const waitForData = waitForDOM(document, "#messages-view .properties-view"); + const [requestFrame] = frames; + EventUtils.sendMouseEvent({ type: "mousedown" }, requestFrame); + + await waitForData; + + is( + document.querySelector("#messages-view .data-label").innerText, + "Action Cable", + "The Action Cable payload panel should be displayed" + ); + + ok( + document.querySelector("#messages-view .treeTable"), + "A tree table should be used to display the formatted payload" + ); + + ok( + document.getElementById("/data"), + "The 'data' property should be displayed" + ); + ok( + document.getElementById("/data/x"), + "The 'x' property in the 'data' object should be displayed" + ); + + // Toggle raw data display + wait = waitForDOM(document, "#messages-view .message-rawData-payload"); + const rawDataToggle = document.querySelector( + "#messages-view .devtools-checkbox-toggle" + ); + clickElement(rawDataToggle, monitor); + await wait; + + is( + document.querySelector("#messages-view .data-label").innerText, + "Raw Data (20 B)", + "The raw data payload info should be displayed" + ); + + is( + document.querySelector("#messages-view .message-rawData-payload").value, + `{"data":"{\\"x\\":2}"}`, + "The raw data must be shown correctly" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-json-payload.js b/devtools/client/netmonitor/test/browser_net_ws-json-payload.js new file mode 100644 index 0000000000..8980dde313 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-json-payload.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WebSocket payloads containing a basic JSON message is parsed + * correctly and displayed in a user friendly way + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(0); + content.wrappedJSObject.sendData( + `{\"foo\":{\"x\":1,\"y\":2}, \"bar\":{\"x\":1,\"y\":2}}` + ); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for all sent/received messages to be displayed in DevTools + let wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + await clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + // Wait for next tick to do async stuff (The MessagePayload component uses the async function getMessagePayload) + await waitForTick(); + const waitForData = waitForDOM(document, "#messages-view .properties-view"); + const [requestFrame] = frames; + EventUtils.sendMouseEvent({ type: "mousedown" }, requestFrame); + + await waitForData; + + is( + document.querySelector("#messages-view .data-label").innerText, + "JSON", + "The JSON payload panel should be displayed" + ); + + ok( + document.querySelector("#messages-view .treeTable"), + "A tree table should be used to display the formatted payload" + ); + + ok(document.getElementById("/foo"), "The 'foo' property should be displayed"); + ok( + document.getElementById("/foo/x"), + "The 'x' property in the `foo` object should be displayed" + ); + ok( + document.getElementById("/bar/y"), + "The 'y' property in the `bar` object should be displayed" + ); + + // Toggle raw data display + wait = waitForDOM(document, "#messages-view .message-rawData-payload"); + const rawDataToggle = document.querySelector( + "#messages-view .devtools-checkbox-toggle" + ); + clickElement(rawDataToggle, monitor); + await wait; + + is( + document.querySelector("#messages-view .data-label").innerText, + "Raw Data (42 B)", + "The raw data payload info should be displayed" + ); + + is( + document.querySelector("#messages-view .message-rawData-payload").value, + `{\"foo\":{\"x\":1,\"y\":2}, \"bar\":{\"x\":1,\"y\":2}}`, + "The raw data must be shown correctly" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-json-stomp-payload.js b/devtools/client/netmonitor/test/browser_net_ws-json-stomp-payload.js new file mode 100644 index 0000000000..74c5d08dd0 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-json-stomp-payload.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WebSocket payloads containing an array of STOMP messages are parsed + * correctly and displayed in a user friendly way + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(0); + content.wrappedJSObject.sendData( + `[\"SEND\\nx-firefox-test:true\\ncontent-length:17\\n\\n[{\\\"key\\\":\\\"value\\\"}]\\u0000\"]` + ); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for all sent/received messages to be displayed in DevTools + let wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + // Wait for next tick to do async stuff (The MessagePayload component uses the async function getMessagePayload) + await waitForTick(); + const waitForData = waitForDOM(document, "#messages-view .properties-view"); + const [requestFrame] = frames; + EventUtils.sendMouseEvent({ type: "mousedown" }, requestFrame); + + await waitForData; + + is( + document.querySelector("#messages-view .data-label").innerText, + "JSON", + "The JSON payload panel should be displayed" + ); + + ok( + document.querySelector("#messages-view .treeTable"), + "A tree table should be used to display the formatted payload" + ); + + ok( + document.getElementById("/0/command"), + "The message 'command' should be displayed" + ); + ok( + document.getElementById("/0/headers"), + "The message 'headers' should be displayed" + ); + ok( + document.getElementById("/0/body"), + "The message 'body' should be displayed" + ); + + // Toggle raw data display + wait = waitForDOM(document, "#messages-view .message-rawData-payload"); + const rawDataToggle = document.querySelector( + "#messages-view .devtools-checkbox-toggle" + ); + clickElement(rawDataToggle, monitor); + await wait; + + is( + document.querySelector("#messages-view .data-label").innerText, + "Raw Data (79 B)", + "The raw data payload info should be displayed" + ); + + is( + document.querySelector("#messages-view .message-rawData-payload").value, + `[\"SEND\\nx-firefox-test:true\\ncontent-length:17\\n\\n[{\\\"key\\\":\\\"value\\\"}]\\u0000\"]`, + "The raw data must be shown correctly" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-keep-future-frames.js b/devtools/client/netmonitor/test/browser_net_ws-keep-future-frames.js new file mode 100644 index 0000000000..a4eda01d0b --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-keep-future-frames.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + // Set WS messages limit to a lower value for testing + await pushPref("devtools.netmonitor.msg.displayed-messages.limit", 10); + + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(10); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for truncated message notification to appear + const truncatedMessageWait = waitForDOM( + document, + "#messages-view .truncated-message" + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await truncatedMessageWait; + + // Wait for truncated message notification to appear and to have an expected text + await waitFor(() => + document + .querySelector("#messages-view .truncated-message") + .textContent.includes("10 messages") + ); + + // Set on 'Keep all future messages' checkbox + const truncationCheckbox = document.querySelector( + "#messages-view .truncation-checkbox" + ); + truncationCheckbox.click(); + + // Get rid of all current messages + const clearButton = document.querySelector( + "#messages-view .message-list-clear-button" + ); + clearButton.click(); + + // And request new messages + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.sendFrames(10); + }); + + // Wait while they appear in the Response tab + // We should see all requested messages (sometimes more than 20 = 10 sent + 10 received) + await waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 20 + ); + + const truncatedMessage = document.querySelector( + "#messages-view .truncated-message" + ).textContent; + + is(truncatedMessage, "0 messages have been truncated to conserve memory"); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-limit-frames.js b/devtools/client/netmonitor/test/browser_net_ws-limit-frames.js new file mode 100644 index 0000000000..1c9ee009cd --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-limit-frames.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS connection is established successfully and the truncated message notification displays correctly. + */ + +add_task(async function () { + // Set WS messages limit to a lower value for testing + await pushPref("devtools.netmonitor.msg.displayed-messages.limit", 30); + + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(20); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for truncated message notification to appear + const wait = waitForDOM(document, "#messages-view .truncated-message"); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 30, "There should be thirty frames"); + is( + document.querySelectorAll("#messages-view .truncated-message").length, + 1, + "Truncated message notification is shown" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-limit-payload.js b/devtools/client/netmonitor/test/browser_net_ws-limit-payload.js new file mode 100644 index 0000000000..8a3082810a --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-limit-payload.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS connection is established successfully and the truncated payload is correct. + */ + +add_task(async function () { + // Set WS message payload limit to a lower value for testing + await pushPref("devtools.netmonitor.msg.messageDataLimit", 100); + + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(0); + content.wrappedJSObject.sendData(new Array(10 * 11).toString()); // > 100B payload + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for all sent/received messages to be displayed in DevTools + const wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + // Wait for next tick to do async stuff (The MessagePayload component uses the async function getMessagePayload) + await waitForTick(); + EventUtils.sendMouseEvent({ type: "mousedown" }, frames[0]); + + await waitForDOM(document, "#messages-view .truncated-data-message"); + + ok( + document.querySelector("#messages-view .truncated-data-message"), + "Truncated data header shown" + ); + is( + document.querySelector("#messages-view .message-rawData-payload") + .textContent.length, + 100, + "Payload size is kept to the limit" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-messages-navigation.js b/devtools/client/netmonitor/test/browser_net_ws-messages-navigation.js new file mode 100644 index 0000000000..4555dc27eb --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-messages-navigation.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WS messages can be navigated between using the keyboard. + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(0); + // Send 5 WS messages + Array(5) + .fill(undefined) + .forEach((_, index) => { + content.wrappedJSObject.sendData(index); + }); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for all sent/received messages to be displayed in DevTools + const wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 10 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 10, "There should be ten frames"); + // Wait for next tick to do async stuff (The MessagePayload component uses the async function getMessagePayload) + await waitForTick(); + + const waitForSelected = waitForDOM( + document, + // The first message is actually the second child, there is a hidden row. + `.message-list-item:nth-child(${2}).selected` + ); + EventUtils.sendMouseEvent({ type: "mousedown" }, frames[0]); + await waitForSelected; + + const checkSelected = messageRowNumber => { + is( + Array.from(frames).findIndex(el => el.matches(".selected")), + messageRowNumber - 1, + `Message ${messageRowNumber} should be selected.` + ); + }; + + // Need focus for keyboard shortcuts to work + frames[0].focus(); + + checkSelected(1); + + EventUtils.sendKey("DOWN", window); + checkSelected(2); + + EventUtils.sendKey("UP", window); + checkSelected(1); + + EventUtils.sendKey("PAGE_DOWN", window); + checkSelected(3); + + EventUtils.sendKey("PAGE_UP", window); + checkSelected(1); + + EventUtils.sendKey("END", window); + checkSelected(10); + + EventUtils.sendKey("HOME", window); + checkSelected(1); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-sockjs-stomp-payload.js b/devtools/client/netmonitor/test/browser_net_ws-sockjs-stomp-payload.js new file mode 100644 index 0000000000..2fe8e6e949 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-sockjs-stomp-payload.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WebSocket payloads containing SockJS+STOMP messages are parsed + * correctly and displayed in a user friendly way + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(0); + content.wrappedJSObject.sendData( + `a[\"SEND\\nx-firefox-test:true\\ncontent-length:17\\n\\n[{\\\"key\\\":\\\"value\\\"}]\\u0000\"]` + ); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one requests"); + + // Wait for all sent/received messages to be displayed in DevTools + let wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + // Wait for next tick to do async stuff (The MessagePayload component uses the async function getMessagePayload) + await waitForTick(); + const waitForData = waitForDOM(document, "#messages-view .properties-view"); + const [, responseFrame] = frames; + EventUtils.sendMouseEvent({ type: "mousedown" }, responseFrame); + + await waitForData; + + is( + document.querySelector("#messages-view .data-label").innerText, + "SockJS", + "The SockJS payload panel should be displayed" + ); + + ok( + document.querySelector("#messages-view .treeTable"), + "A tree table should be used to display the formatted payload" + ); + + ok( + document.getElementById("/0/command"), + "The message 'command' should be displayed" + ); + ok( + document.getElementById("/0/headers"), + "The message 'headers' should be displayed" + ); + ok( + document.getElementById("/0/body"), + "The message 'body' should be displayed" + ); + + // Toggle raw data display + wait = waitForDOM(document, "#messages-view .message-rawData-payload"); + const rawDataToggle = document.querySelector( + "#messages-view .devtools-checkbox-toggle" + ); + clickElement(rawDataToggle, monitor); + await wait; + + is( + document.querySelector("#messages-view .data-label").innerText, + "Raw Data (80 B)", + "The raw data payload info should be displayed" + ); + + is( + document.querySelector("#messages-view .message-rawData-payload").value, + `a[\"SEND\\nx-firefox-test:true\\ncontent-length:17\\n\\n[{\\\"key\\\":\\\"value\\\"}]\\u0000\"]`, + "The raw data must be shown correctly" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-sse-persist-columns.js b/devtools/client/netmonitor/test/browser_net_ws-sse-persist-columns.js new file mode 100644 index 0000000000..66a7f36e74 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-sse-persist-columns.js @@ -0,0 +1,268 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test columns' state for WS and SSE connection. + */ + +function shallowArrayEqual(arr1, arr2) { + if (arr1.length !== arr2.length) { + return false; + } + for (let i = 0; i < arr1.length; i++) { + if ( + (arr2[i] instanceof RegExp && !arr2[i].test(arr1[i])) || + (typeof arr2[i] === "string" && arr2[i] !== arr1[i]) + ) { + return false; + } + } + return true; +} + +function shallowObjectEqual(obj1, obj2) { + const k1 = Object.keys(obj1); + const k2 = Object.keys(obj2); + + if (k1.length !== k2.length) { + return false; + } + + for (const key of k1) { + if (obj1[key] !== obj2[key]) { + return false; + } + } + + return true; +} + +function shallowEqual(obj1, obj2) { + if (Array.isArray(obj1) && Array.isArray(obj2)) { + return shallowArrayEqual(obj1, obj2); + } + return shallowObjectEqual(obj1, obj2); +} + +add_task(async function () { + const { tab, monitor } = await initNetMonitor( + "http://mochi.test:8888/browser/devtools/client/netmonitor/test/html_ws-sse-test-page.html", + { + requestCount: 1, + } + ); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + const onNetworkEvents = waitForNetworkEvents(monitor, 2); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openWsConnection(1); + // Running openSseConnection() here causes intermittent behavior. + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openSseConnection(); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 2, "There should be two requests"); + + // Select the WS request. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + store.dispatch(Actions.toggleMessageColumn("size")); + store.dispatch(Actions.toggleMessageColumn("opCode")); + store.dispatch(Actions.toggleMessageColumn("maskBit")); + store.dispatch(Actions.toggleMessageColumn("finBit")); + clickOnSidebarTab(document, "response"); + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + let columnHeaders = Array.prototype.map.call( + document.querySelectorAll( + "#messages-view .message-list-headers .button-text" + ), + node => node.textContent + ); + + is( + shallowEqual(columnHeaders, [ + "Data", + "Size", + "OpCode", + "MaskBit", + "FinBit", + "Time", + ]), + true, + "WS Column headers are in correct order" + ); + + // Get column values of first row for WS. + let columnValues = Array.prototype.map.call(frames, frame => + Array.prototype.map.call( + frame.querySelectorAll(".message-list-column"), + column => column.textContent.trim() + ) + )[0]; + + is( + shallowEqual(columnValues, [ + "Payload 0", + "9 B", + "1", + "true", + "true", + // Time format is "hh:mm:ss.mmm". + /\d+:\d+:\d+\.\d+/, + ]), + true, + "WS Column values are in correct order" + ); + + // Select the SSE request. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[1]); + + store.dispatch(Actions.toggleMessageColumn("lastEventId")); + store.dispatch(Actions.toggleMessageColumn("eventName")); + store.dispatch(Actions.toggleMessageColumn("retry")); + + columnHeaders = Array.prototype.map.call( + document.querySelectorAll( + "#messages-view .message-list-headers .button-text" + ), + node => node.textContent + ); + + is( + shallowEqual(columnHeaders, [ + "Data", + "Time", + "Event Name", + "Last Event ID", + "Retry", + ]), + true, + "SSE Column headers are in correct order" + ); + + // Get column values of first row for SSE. + columnValues = Array.prototype.map.call(frames, frame => + Array.prototype.map.call( + frame.querySelectorAll(".message-list-column"), + column => column.textContent.trim() + ) + )[0]; + + is( + shallowEqual(columnValues, [ + "Why so serious?", + /\d+:\d+:\d+\.\d+/, + "message", + "", + "5000", + ]), + true, + "SSE Column values are in correct order" + ); + + // Select the WS request again. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + is( + shallowEqual(store.getState().messages.columns, { + data: true, + time: true, + size: true, + opCode: true, + maskBit: true, + finBit: true, + }), + true, + "WS columns should persist after request switch" + ); + + // Select the SSE request again. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[1]); + is( + shallowEqual(store.getState().messages.columns, { + data: true, + time: true, + size: false, + lastEventId: true, + eventName: true, + retry: true, + }), + true, + "SSE columns should persist after request switch" + ); + + // Reset SSE columns. + store.dispatch(Actions.resetMessageColumns()); + + // Switch to WS request again. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + is( + shallowEqual(store.getState().messages.columns, { + data: true, + time: true, + size: true, + opCode: true, + maskBit: true, + finBit: true, + }), + true, + "WS columns should not reset after resetting SSE columns" + ); + + // Reset WS columns. + store.dispatch(Actions.resetMessageColumns()); + + // Switch to SSE request again. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[1]); + is( + shallowEqual(store.getState().messages.columns, { + data: true, + time: true, + size: false, + lastEventId: false, + eventName: false, + retry: false, + }), + true, + "SSE columns' reset state should persist after request switch" + ); + + // Switch to WS request again. + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + is( + shallowEqual(store.getState().messages.columns, { + data: true, + time: true, + size: false, + opCode: false, + maskBit: false, + finBit: false, + }), + true, + "WS columns' reset state should persist after request switch" + ); + + // Close WS connection. + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeWsConnection(); + }); + + return teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/browser_net_ws-stomp-payload.js b/devtools/client/netmonitor/test/browser_net_ws-stomp-payload.js new file mode 100644 index 0000000000..c3bd284c7b --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_ws-stomp-payload.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that WebSocket payloads containing a STOMP formatted message are parsed + * correctly and displayed in a user friendly way + */ + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(WS_PAGE_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { document, store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + store.dispatch(Actions.batchEnable(false)); + + // Wait for WS connections to be established + send messages + const onNetworkEvents = waitForNetworkEvents(monitor, 1); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.openConnection(0); + content.wrappedJSObject.sendData( + `SEND\nx-firefox-test:true\ncontent-length:17\n\n[{"key":"value"}]\u0000\n` + ); + }); + await onNetworkEvents; + + const requests = document.querySelectorAll(".request-list-item"); + is(requests.length, 1, "There should be one request"); + + // Wait for all sent/received messages to be displayed in DevTools + let wait = waitForDOM( + document, + "#messages-view .message-list-table .message-list-item", + 2 + ); + + // Select the first request + EventUtils.sendMouseEvent({ type: "mousedown" }, requests[0]); + + // Click on the "Response" panel + clickOnSidebarTab(document, "response"); + await wait; + + // Get all messages present in the "Response" panel + const frames = document.querySelectorAll( + "#messages-view .message-list-table .message-list-item" + ); + + // Check expected results + is(frames.length, 2, "There should be two frames"); + + // Wait for next tick to do async stuff (The MessagePayload component uses the async function getMessagePayload) + await waitForTick(); + const waitForData = waitForDOM(document, "#messages-view .properties-view"); + const [requestFrame] = frames; + EventUtils.sendMouseEvent({ type: "mousedown" }, requestFrame); + + await waitForData; + + is( + document.querySelector("#messages-view .data-label").innerText, + "STOMP", + "The STOMP payload panel should be displayed" + ); + + ok( + document.querySelector("#messages-view .treeTable"), + "A tree table should be used to display the formatted payload" + ); + + ok( + document.getElementById("/command"), + "The message 'command' should be displayed" + ); + ok( + document.getElementById("/headers"), + "The message 'headers' should be displayed" + ); + ok( + document.getElementById("/body"), + "The message 'body' should be displayed" + ); + + // Toggle raw data display + wait = waitForDOM(document, "#messages-view .message-rawData-payload"); + const rawDataToggle = document.querySelector( + "#messages-view .devtools-checkbox-toggle" + ); + clickElement(rawDataToggle, monitor); + await wait; + + is( + document.querySelector("#messages-view .data-label").innerText, + "Raw Data (63 B)", + "The raw data payload info should be displayed" + ); + + is( + document.querySelector("#messages-view .message-rawData-payload").value, + `SEND\nx-firefox-test:true\ncontent-length:17\n\n[{"key":"value"}]\u0000\n`, + "The raw data must be shown correctly" + ); + + // Close WS connection + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.closeConnection(); + }); + + await teardown(monitor); +}); diff --git a/devtools/client/netmonitor/test/dropmarker.svg b/devtools/client/netmonitor/test/dropmarker.svg new file mode 100644 index 0000000000..3e2987682b --- /dev/null +++ b/devtools/client/netmonitor/test/dropmarker.svg @@ -0,0 +1,6 @@ +<!-- 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" width="8" height="4" viewBox="0 0 8 4"> + <polygon points="0,0 4,4 8,0" fill="#b6babf"/> +</svg> diff --git a/devtools/client/netmonitor/test/file_ws_backend_wsh.py b/devtools/client/netmonitor/test/file_ws_backend_wsh.py new file mode 100644 index 0000000000..f09f4dfed7 --- /dev/null +++ b/devtools/client/netmonitor/test/file_ws_backend_wsh.py @@ -0,0 +1,13 @@ +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + while not request.client_terminated: + resp = msgutil.receive_message(request) + msgutil.send_message(request, resp, binary=isinstance(resp, bytes)) + + msgutil.close_connection(request) diff --git a/devtools/client/netmonitor/test/head.js b/devtools/client/netmonitor/test/head.js new file mode 100644 index 0000000000..5ca3466964 --- /dev/null +++ b/devtools/client/netmonitor/test/head.js @@ -0,0 +1,1546 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file (head.js) is injected into all other test contexts within + * this directory, allowing one to utilize the functions here in said + * tests without referencing head.js explicitly. + */ + +/* exported Toolbox, restartNetMonitor, teardown, waitForExplicitFinish, + verifyRequestItemTarget, waitFor, waitForDispatch, testFilterButtons, + performRequestsInContent, waitForNetworkEvents, selectIndexAndWaitForSourceEditor, + testColumnsAlignment, hideColumn, showColumn, performRequests, waitForRequestData, + toggleBlockedUrl, registerFaviconNotifier, clickOnSidebarTab */ + +"use strict"; + +// The below file (shared-head.js) handles imports, constants, and +// utility functions, and is loaded into this context. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); + +const { LinkHandlerParent } = ChromeUtils.importESModule( + "resource:///actors/LinkHandlerParent.sys.mjs" +); + +const { + getFormattedIPAndPort, + getFormattedTime, +} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); + +const { + getSortedRequests, + getRequestById, +} = require("resource://devtools/client/netmonitor/src/selectors/index.js"); + +const { + getUnicodeUrl, + getUnicodeHostname, +} = require("resource://devtools/client/shared/unicode-url.js"); +const { + getFormattedProtocol, + getUrlHost, + getUrlScheme, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); +const { + EVENTS, + TEST_EVENTS, +} = require("resource://devtools/client/netmonitor/src/constants.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + +/* eslint-disable no-unused-vars, max-len */ +const EXAMPLE_URL = + "http://example.com/browser/devtools/client/netmonitor/test/"; +const EXAMPLE_ORG_URL = + "http://example.org/browser/devtools/client/netmonitor/test/"; +const HTTPS_EXAMPLE_URL = + "https://example.com/browser/devtools/client/netmonitor/test/"; +const HTTPS_EXAMPLE_ORG_URL = + "https://example.org/browser/devtools/client/netmonitor/test/"; +/* Since the test server will proxy `ws://example.com` to websocket server on 9988, +so we must sepecify the port explicitly */ +const WS_URL = "ws://127.0.0.1:8888/browser/devtools/client/netmonitor/test/"; +const WS_HTTP_URL = + "http://127.0.0.1:8888/browser/devtools/client/netmonitor/test/"; + +const WS_BASE_URL = + "http://mochi.test:8888/browser/devtools/client/netmonitor/test/"; +const WS_PAGE_URL = WS_BASE_URL + "html_ws-test-page.html"; +const WS_PAGE_EARLY_CONNECTION_URL = + WS_BASE_URL + "html_ws-early-connection-page.html"; +const API_CALLS_URL = HTTPS_EXAMPLE_URL + "html_api-calls-test-page.html"; +const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html"; +const HTTPS_SIMPLE_URL = HTTPS_EXAMPLE_URL + "html_simple-test-page.html"; +const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html"; +const CONTENT_TYPE_WITHOUT_CACHE_URL = + EXAMPLE_URL + "html_content-type-without-cache-test-page.html"; +const CONTENT_TYPE_WITHOUT_CACHE_REQUESTS = 8; +const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html"; +const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html"; +const HTTPS_STATUS_CODES_URL = + HTTPS_EXAMPLE_URL + "html_status-codes-test-page.html"; +const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html"; +const POST_ARRAY_DATA_URL = EXAMPLE_URL + "html_post-array-data-test-page.html"; +const POST_JSON_URL = EXAMPLE_URL + "html_post-json-test-page.html"; +const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html"; +const POST_RAW_URL_WITH_HASH = EXAMPLE_URL + "html_header-test-page.html"; +const POST_RAW_WITH_HEADERS_URL = + EXAMPLE_URL + "html_post-raw-with-headers-test-page.html"; +const PARAMS_URL = EXAMPLE_URL + "html_params-test-page.html"; +const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html"; +const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html"; +const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html"; +const JSON_CUSTOM_MIME_URL = + EXAMPLE_URL + "html_json-custom-mime-test-page.html"; +const JSON_TEXT_MIME_URL = EXAMPLE_URL + "html_json-text-mime-test-page.html"; +const JSON_B64_URL = EXAMPLE_URL + "html_json-b64.html"; +const JSON_BASIC_URL = EXAMPLE_URL + "html_json-basic.html"; +const JSON_EMPTY_URL = EXAMPLE_URL + "html_json-empty.html"; +const JSON_XSSI_PROTECTION_URL = EXAMPLE_URL + "html_json-xssi-protection.html"; +const FONTS_URL = EXAMPLE_URL + "html_fonts-test-page.html"; +const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html"; +const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html"; +const HTTPS_FILTERING_URL = HTTPS_EXAMPLE_URL + "html_filter-test-page.html"; +const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html"; +const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html"; +const HTTPS_CUSTOM_GET_URL = HTTPS_EXAMPLE_URL + "html_custom-get-page.html"; +const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html"; +const HTTPS_SINGLE_GET_URL = HTTPS_EXAMPLE_URL + "html_single-get-page.html"; +const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html"; +const STATISTICS_EDGE_CASE_URL = + EXAMPLE_URL + "html_statistics-edge-case-page.html"; +const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html"; +const HTTPS_CURL_URL = HTTPS_EXAMPLE_URL + "html_copy-as-curl.html"; +const HTTPS_CURL_UTILS_URL = HTTPS_EXAMPLE_URL + "html_curl-utils.html"; +const SEND_BEACON_URL = EXAMPLE_URL + "html_send-beacon.html"; +const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html"; +const HTTPS_CORS_URL = HTTPS_EXAMPLE_URL + "html_cors-test-page.html"; +const PAUSE_URL = EXAMPLE_URL + "html_pause-test-page.html"; +const OPEN_REQUEST_IN_TAB_URL = EXAMPLE_URL + "html_open-request-in-tab.html"; +const CSP_URL = EXAMPLE_URL + "html_csp-test-page.html"; +const CSP_RESEND_URL = EXAMPLE_URL + "html_csp-resend-test-page.html"; +const IMAGE_CACHE_URL = HTTPS_EXAMPLE_URL + "html_image-cache.html"; +const SLOW_REQUESTS_URL = EXAMPLE_URL + "html_slow-requests-test-page.html"; + +const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs"; +const HTTPS_SIMPLE_SJS = HTTPS_EXAMPLE_URL + "sjs_simple-test-server.sjs"; +const SIMPLE_UNSORTED_COOKIES_SJS = + EXAMPLE_URL + "sjs_simple-unsorted-cookies-test-server.sjs"; +const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs"; +const WS_CONTENT_TYPE_SJS = WS_HTTP_URL + "sjs_content-type-test-server.sjs"; +const WS_WS_CONTENT_TYPE_SJS = WS_URL + "sjs_content-type-test-server.sjs"; +const HTTPS_CONTENT_TYPE_SJS = + HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs"; +const SERVER_TIMINGS_TYPE_SJS = + HTTPS_EXAMPLE_URL + "sjs_timings-test-server.sjs"; +const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs"; +const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs"; +const HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs"; +const CORS_SJS_PATH = + "/browser/devtools/client/netmonitor/test/sjs_cors-test-server.sjs"; +const HSTS_SJS = EXAMPLE_URL + "sjs_hsts-test-server.sjs"; +const METHOD_SJS = EXAMPLE_URL + "sjs_method-test-server.sjs"; +const HTTPS_SLOW_SJS = HTTPS_EXAMPLE_URL + "sjs_slow-test-server.sjs"; +const SET_COOKIE_SAME_SITE_SJS = EXAMPLE_URL + "sjs_set-cookie-same-site.sjs"; +const SEARCH_SJS = EXAMPLE_URL + "sjs_search-test-server.sjs"; +const HTTPS_SEARCH_SJS = HTTPS_EXAMPLE_URL + "sjs_search-test-server.sjs"; + +const HSTS_BASE_URL = EXAMPLE_URL; +const HSTS_PAGE_URL = CUSTOM_GET_URL; + +const TEST_IMAGE = EXAMPLE_URL + "test-image.png"; +const TEST_IMAGE_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; + +const SETTINGS_MENU_ITEMS = { + "persist-logs": ".netmonitor-settings-persist-item", + "import-har": ".netmonitor-settings-import-har-item", + "save-har": ".netmonitor-settings-import-save-item", + "copy-har": ".netmonitor-settings-import-copy-item", +}; + +/* eslint-enable no-unused-vars, max-len */ + +// All tests are asynchronous. +waitForExplicitFinish(); + +const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +// To enable logging for try runs, just set the pref to true. +Services.prefs.setBoolPref("devtools.debugger.log", false); + +// Uncomment this pref to dump all devtools emitted events to the console. +// Services.prefs.setBoolPref("devtools.dump.emit", true); + +// Always reset some prefs to their original values after the test finishes. +const gDefaultFilters = Services.prefs.getCharPref( + "devtools.netmonitor.filters" +); + +// Reveal many columns for test +Services.prefs.setCharPref( + "devtools.netmonitor.visibleColumns", + '["initiator","contentSize","cookies","domain","duration",' + + '"endTime","file","url","latency","method","protocol",' + + '"remoteip","responseTime","scheme","setCookies",' + + '"startTime","status","transferred","type","waterfall"]' +); + +Services.prefs.setCharPref( + "devtools.netmonitor.columnsData", + '[{"name":"status","minWidth":30,"width":5},' + + '{"name":"method","minWidth":30,"width":5},' + + '{"name":"domain","minWidth":30,"width":10},' + + '{"name":"file","minWidth":30,"width":25},' + + '{"name":"url","minWidth":30,"width":25},' + + '{"name":"initiator","minWidth":30,"width":20},' + + '{"name":"type","minWidth":30,"width":5},' + + '{"name":"transferred","minWidth":30,"width":10},' + + '{"name":"contentSize","minWidth":30,"width":5},' + + '{"name":"waterfall","minWidth":150,"width":15}]' +); + +registerCleanupFunction(() => { + info("finish() was called, cleaning up..."); + + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + Services.prefs.setCharPref("devtools.netmonitor.filters", gDefaultFilters); + Services.prefs.clearUserPref("devtools.cache.disabled"); + Services.prefs.clearUserPref("devtools.netmonitor.columnsData"); + Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns"); + Services.cookies.removeAll(); +}); + +async function disableCacheAndReload(toolbox, waitForLoad) { + // Disable the cache for any toolbox that it is opened from this point on. + Services.prefs.setBoolPref("devtools.cache.disabled", true); + + await toolbox.commands.targetConfigurationCommand.updateConfiguration({ + cacheDisabled: true, + }); + + // If the page which is reloaded is not found, this will likely cause + // reloadTopLevelTarget to not return so let not wait for it. + if (waitForLoad) { + await toolbox.commands.targetCommand.reloadTopLevelTarget(); + } else { + toolbox.commands.targetCommand.reloadTopLevelTarget(); + } +} + +/** + * Wait for 2 markers during document load. + */ +function waitForTimelineMarkers(monitor) { + return new Promise(resolve => { + const markers = []; + + function handleTimelineEvent(marker) { + info(`Got marker: ${marker.name}`); + markers.push(marker); + if (markers.length == 2) { + monitor.panelWin.api.off( + TEST_EVENTS.TIMELINE_EVENT, + handleTimelineEvent + ); + info("Got two timeline markers, done waiting"); + resolve(markers); + } + } + + monitor.panelWin.api.on(TEST_EVENTS.TIMELINE_EVENT, handleTimelineEvent); + }); +} + +let finishedQueue = {}; +const updatingTypes = [ + "NetMonitor:NetworkEventUpdating:RequestCookies", + "NetMonitor:NetworkEventUpdating:ResponseCookies", + "NetMonitor:NetworkEventUpdating:RequestHeaders", + "NetMonitor:NetworkEventUpdating:ResponseHeaders", + "NetMonitor:NetworkEventUpdating:RequestPostData", + "NetMonitor:NetworkEventUpdating:ResponseContent", + "NetMonitor:NetworkEventUpdating:SecurityInfo", + "NetMonitor:NetworkEventUpdating:EventTimings", +]; +const updatedTypes = [ + "NetMonitor:NetworkEventUpdated:RequestCookies", + "NetMonitor:NetworkEventUpdated:ResponseCookies", + "NetMonitor:NetworkEventUpdated:RequestHeaders", + "NetMonitor:NetworkEventUpdated:ResponseHeaders", + "NetMonitor:NetworkEventUpdated:RequestPostData", + "NetMonitor:NetworkEventUpdated:ResponseContent", + "NetMonitor:NetworkEventUpdated:SecurityInfo", + "NetMonitor:NetworkEventUpdated:EventTimings", +]; + +// Start collecting all networkEventUpdate event when panel is opened. +// removeTab() should be called once all corresponded RECEIVED_* events finished. +function startNetworkEventUpdateObserver(panelWin) { + updatingTypes.forEach(type => + panelWin.api.on(type, actor => { + const key = actor + "-" + updatedTypes[updatingTypes.indexOf(type)]; + finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] + 1 : 1; + }) + ); + + updatedTypes.forEach(type => + panelWin.api.on(type, payload => { + const key = payload.from + "-" + type; + finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] - 1 : -1; + }) + ); + + panelWin.api.on("clear-network-resources", () => { + finishedQueue = {}; + }); +} + +async function waitForAllNetworkUpdateEvents() { + function checkNetworkEventUpdateState() { + for (const key in finishedQueue) { + if (finishedQueue[key] > 0) { + return false; + } + } + return true; + } + info("Wait for completion of all NetworkUpdateEvents packets..."); + await waitUntil(() => checkNetworkEventUpdateState()); + finishedQueue = {}; +} + +function initNetMonitor( + url, + { + requestCount, + expectedEventTimings, + waitForLoad = true, + enableCache = false, + } +) { + info("Initializing a network monitor pane."); + + if (!requestCount && !enableCache) { + ok( + false, + "initNetMonitor should be given a number of requests the page will perform" + ); + } + + return (async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Capture all stacks so that the timing of devtools opening + // doesn't affect the stack trace results. + ["javascript.options.asyncstack_capture_debuggee_only", false], + ], + }); + + const tab = await addTab(url, { waitForLoad }); + info("Net tab added successfully: " + url); + + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "netmonitor", + }); + info("Network monitor pane shown successfully."); + + const monitor = toolbox.getCurrentPanel(); + + startNetworkEventUpdateObserver(monitor.panelWin); + + if (!enableCache) { + info("Disabling cache and reloading page."); + + const allComplete = []; + allComplete.push( + waitForNetworkEvents(monitor, requestCount, { + expectedEventTimings, + }) + ); + + if (waitForLoad) { + allComplete.push(waitForTimelineMarkers(monitor)); + } + await disableCacheAndReload(toolbox, waitForLoad); + await Promise.all(allComplete); + await clearNetworkEvents(monitor); + } + + return { tab, monitor, toolbox }; + })(); +} + +function restartNetMonitor(monitor, { requestCount }) { + info("Restarting the specified network monitor."); + + return (async function () { + const tab = monitor.commands.descriptorFront.localTab; + const url = tab.linkedBrowser.currentURI.spec; + + await waitForAllNetworkUpdateEvents(); + info("All pending requests finished."); + + const onDestroyed = monitor.once("destroyed"); + await removeTab(tab); + await onDestroyed; + + return initNetMonitor(url, { requestCount }); + })(); +} + +/** + * Clears the network requests in the UI + * @param {Object} monitor + * The netmonitor instance used for retrieving a context menu element. + */ +async function clearNetworkEvents(monitor) { + const { store, windowRequire } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + + await waitForAllNetworkUpdateEvents(); + + info("Clearing the network requests in the UI"); + store.dispatch(Actions.clearRequests()); +} + +function teardown(monitor) { + info("Destroying the specified network monitor."); + + return (async function () { + const tab = monitor.commands.descriptorFront.localTab; + + await waitForAllNetworkUpdateEvents(); + info("All pending requests finished."); + + await monitor.toolbox.destroy(); + await removeTab(tab); + })(); +} + +/** + * Wait for the request(s) to be fully notified to the frontend. + * + * @param {Object} monitor + * The netmonitor instance used for retrieving a context menu element. + * @param {Number} getRequests + * The number of request to wait for + * @param {Object} options (optional) + * - expectedEventTimings {Number} Number of EVENT_TIMINGS events to wait for. + * In case of filtering, we get less of such events. + */ +function waitForNetworkEvents(monitor, getRequests, options = {}) { + return new Promise(resolve => { + const panel = monitor.panelWin; + let networkEvent = 0; + let nonBlockedNetworkEvent = 0; + let payloadReady = 0; + let eventTimings = 0; + + function onNetworkEvent(resource) { + networkEvent++; + if (!resource.blockedReason) { + nonBlockedNetworkEvent++; + } + maybeResolve(TEST_EVENTS.NETWORK_EVENT, resource.actor); + } + + function onPayloadReady(resource) { + payloadReady++; + maybeResolve(EVENTS.PAYLOAD_READY, resource.actor); + } + + function onEventTimings(response) { + eventTimings++; + maybeResolve(EVENTS.RECEIVED_EVENT_TIMINGS, response.from); + } + + function onClearNetworkResources() { + // Reset all counters. + networkEvent = 0; + nonBlockedNetworkEvent = 0; + payloadReady = 0; + eventTimings = 0; + } + + function maybeResolve(event, actor) { + const { document } = monitor.panelWin; + // Wait until networkEvent, payloadReady and event timings finish for each request. + // The UI won't fetch timings when: + // * hidden in background, + // * for any blocked request, + let expectedEventTimings = + document.visibilityState == "hidden" ? 0 : nonBlockedNetworkEvent; + let expectedPayloadReady = getRequests; + // Typically ignore this option if it is undefined or null + if (typeof options?.expectedEventTimings == "number") { + expectedEventTimings = options.expectedEventTimings; + } + if (typeof options?.expectedPayloadReady == "number") { + expectedPayloadReady = options.expectedPayloadReady; + } + info( + "> Network event progress: " + + "NetworkEvent: " + + networkEvent + + "/" + + getRequests + + ", " + + "PayloadReady: " + + payloadReady + + "/" + + expectedPayloadReady + + ", " + + "EventTimings: " + + eventTimings + + "/" + + expectedEventTimings + + ", " + + "got " + + event + + " for " + + actor + ); + + if ( + networkEvent >= getRequests && + payloadReady >= expectedPayloadReady && + eventTimings >= expectedEventTimings + ) { + panel.api.off(TEST_EVENTS.NETWORK_EVENT, onNetworkEvent); + panel.api.off(EVENTS.PAYLOAD_READY, onPayloadReady); + panel.api.off(EVENTS.RECEIVED_EVENT_TIMINGS, onEventTimings); + panel.api.off("clear-network-resources", onClearNetworkResources); + executeSoon(resolve); + } + } + + panel.api.on(TEST_EVENTS.NETWORK_EVENT, onNetworkEvent); + panel.api.on(EVENTS.PAYLOAD_READY, onPayloadReady); + panel.api.on(EVENTS.RECEIVED_EVENT_TIMINGS, onEventTimings); + panel.api.on("clear-network-resources", onClearNetworkResources); + }); +} + +function verifyRequestItemTarget( + document, + requestList, + requestItem, + method, + url, + data = {} +) { + info("> Verifying: " + method + " " + url + " " + data.toSource()); + + const visibleIndex = requestList.findIndex( + needle => needle.id === requestItem.id + ); + + isnot(visibleIndex, -1, "The requestItem exists"); + info("Visible index of item: " + visibleIndex); + + const { + fuzzyUrl, + status, + statusText, + cause, + type, + fullMimeType, + transferred, + size, + time, + displayedStatus, + } = data; + + const target = document.querySelectorAll(".request-list-item")[visibleIndex]; + + // Bug 1414981 - Request URL should not show #hash + const unicodeUrl = getUnicodeUrl(url.split("#")[0]); + const ORIGINAL_FILE_URL = L10N.getFormatStr( + "netRequest.originalFileURL.tooltip", + url + ); + const DECODED_FILE_URL = L10N.getFormatStr( + "netRequest.decodedFileURL.tooltip", + unicodeUrl + ); + const fileToolTip = + url === unicodeUrl ? url : ORIGINAL_FILE_URL + "\n\n" + DECODED_FILE_URL; + const requestedFile = requestItem.urlDetails.baseNameWithQuery; + const host = getUnicodeHostname(getUrlHost(url)); + const scheme = getUrlScheme(url); + const { + remoteAddress, + remotePort, + totalTime, + eventTimings = { timings: {} }, + } = requestItem; + const formattedIPPort = getFormattedIPAndPort(remoteAddress, remotePort); + const remoteIP = remoteAddress ? `${formattedIPPort}` : "unknown"; + const duration = getFormattedTime(totalTime); + const latency = getFormattedTime(eventTimings.timings.wait); + const protocol = getFormattedProtocol(requestItem); + + if (fuzzyUrl) { + ok( + requestItem.method.startsWith(method), + "The attached method is correct." + ); + ok(requestItem.url.startsWith(url), "The attached url is correct."); + } else { + is(requestItem.method, method, "The attached method is correct."); + is(requestItem.url, url.split("#")[0], "The attached url is correct."); + } + + is( + target.querySelector(".requests-list-method").textContent, + method, + "The displayed method is correct." + ); + + if (fuzzyUrl) { + ok( + target + .querySelector(".requests-list-file") + .textContent.startsWith(requestedFile), + "The displayed file is correct." + ); + ok( + target + .querySelector(".requests-list-file") + .getAttribute("title") + .startsWith(fileToolTip), + "The tooltip file is correct." + ); + } else { + is( + target.querySelector(".requests-list-file").textContent, + requestedFile, + "The displayed file is correct." + ); + is( + target.querySelector(".requests-list-file").getAttribute("title"), + fileToolTip, + "The tooltip file is correct." + ); + } + + is( + target.querySelector(".requests-list-protocol").textContent, + protocol, + "The displayed protocol is correct." + ); + + is( + target.querySelector(".requests-list-protocol").getAttribute("title"), + protocol, + "The tooltip protocol is correct." + ); + + is( + target.querySelector(".requests-list-domain").textContent, + host, + "The displayed domain is correct." + ); + + const domainTooltip = + host + (remoteAddress ? " (" + formattedIPPort + ")" : ""); + is( + target.querySelector(".requests-list-domain").getAttribute("title"), + domainTooltip, + "The tooltip domain is correct." + ); + + is( + target.querySelector(".requests-list-remoteip").textContent, + remoteIP, + "The displayed remote IP is correct." + ); + + is( + target.querySelector(".requests-list-remoteip").getAttribute("title"), + remoteIP, + "The tooltip remote IP is correct." + ); + + is( + target.querySelector(".requests-list-scheme").textContent, + scheme, + "The displayed scheme is correct." + ); + + is( + target.querySelector(".requests-list-scheme").getAttribute("title"), + scheme, + "The tooltip scheme is correct." + ); + + is( + target.querySelector(".requests-list-duration-time").textContent, + duration, + "The displayed duration is correct." + ); + + is( + target.querySelector(".requests-list-duration-time").getAttribute("title"), + duration, + "The tooltip duration is correct." + ); + + is( + target.querySelector(".requests-list-latency-time").textContent, + latency, + "The displayed latency is correct." + ); + + is( + target.querySelector(".requests-list-latency-time").getAttribute("title"), + latency, + "The tooltip latency is correct." + ); + + if (status !== undefined) { + const value = target + .querySelector(".requests-list-status-code") + .getAttribute("data-status-code"); + const codeValue = target.querySelector( + ".requests-list-status-code" + ).textContent; + const tooltip = target + .querySelector(".requests-list-status-code") + .getAttribute("title"); + info("Displayed status: " + value); + info("Displayed code: " + codeValue); + info("Tooltip status: " + tooltip); + is( + `${value}`, + displayedStatus ? `${displayedStatus}` : `${status}`, + "The displayed status is correct." + ); + is(`${codeValue}`, `${status}`, "The displayed status code is correct."); + is(tooltip, status + " " + statusText, "The tooltip status is correct."); + } + if (cause !== undefined) { + const value = Array.from( + target.querySelector(".requests-list-initiator").childNodes + ) + .filter(node => node.nodeType === Node.ELEMENT_NODE) + .map(({ textContent }) => textContent) + .join(""); + const tooltip = target + .querySelector(".requests-list-initiator") + .getAttribute("title"); + info("Displayed cause: " + value); + info("Tooltip cause: " + tooltip); + ok(value.includes(cause.type), "The displayed cause is correct."); + ok(tooltip.includes(cause.type), "The tooltip cause is correct."); + } + if (type !== undefined) { + const value = target.querySelector(".requests-list-type").textContent; + let tooltip = target + .querySelector(".requests-list-type") + .getAttribute("title"); + info("Displayed type: " + value); + info("Tooltip type: " + tooltip); + is(value, type, "The displayed type is correct."); + if (Object.is(tooltip, null)) { + tooltip = undefined; + } + is(tooltip, fullMimeType, "The tooltip type is correct."); + } + if (transferred !== undefined) { + const value = target.querySelector( + ".requests-list-transferred" + ).textContent; + const tooltip = target + .querySelector(".requests-list-transferred") + .getAttribute("title"); + info("Displayed transferred size: " + value); + info("Tooltip transferred size: " + tooltip); + is(value, transferred, "The displayed transferred size is correct."); + is(tooltip, transferred, "The tooltip transferred size is correct."); + } + if (size !== undefined) { + const value = target.querySelector(".requests-list-size").textContent; + const tooltip = target + .querySelector(".requests-list-size") + .getAttribute("title"); + info("Displayed size: " + value); + info("Tooltip size: " + tooltip); + is(value, size, "The displayed size is correct."); + is(tooltip, size, "The tooltip size is correct."); + } + if (time !== undefined) { + const value = target.querySelector( + ".requests-list-timings-total" + ).textContent; + const tooltip = target + .querySelector(".requests-list-timings-total") + .getAttribute("title"); + info("Displayed time: " + value); + info("Tooltip time: " + tooltip); + Assert.greaterOrEqual( + ~~value.match(/[0-9]+/), + 0, + "The displayed time is correct." + ); + Assert.greaterOrEqual( + ~~tooltip.match(/[0-9]+/), + 0, + "The tooltip time is correct." + ); + } + + if (visibleIndex !== -1) { + if (visibleIndex % 2 === 0) { + ok(target.classList.contains("even"), "Item should have 'even' class."); + ok(!target.classList.contains("odd"), "Item shouldn't have 'odd' class."); + } else { + ok( + !target.classList.contains("even"), + "Item shouldn't have 'even' class." + ); + ok(target.classList.contains("odd"), "Item should have 'odd' class."); + } + } +} + +/** + * Tests if a button for a filter of given type is the only one checked. + * + * @param string filterType + * The type of the filter that should be the only one checked. + */ +function testFilterButtons(monitor, filterType) { + const doc = monitor.panelWin.document; + const target = doc.querySelector( + ".requests-list-filter-" + filterType + "-button" + ); + ok(target, `Filter button '${filterType}' was found`); + const buttons = [ + ...doc.querySelectorAll(".requests-list-filter-buttons button"), + ]; + ok(!!buttons.length, "More than zero filter buttons were found"); + + // Only target should be checked. + const checkStatus = buttons.map(button => (button == target ? 1 : 0)); + testFilterButtonsCustom(monitor, checkStatus); +} + +/** + * Tests if filter buttons have 'checked' attributes set correctly. + * + * @param array aIsChecked + * An array specifying if a button at given index should have a + * 'checked' attribute. For example, if the third item of the array + * evaluates to true, the third button should be checked. + */ +function testFilterButtonsCustom(monitor, isChecked) { + const doc = monitor.panelWin.document; + const buttons = doc.querySelectorAll(".requests-list-filter-buttons button"); + for (let i = 0; i < isChecked.length; i++) { + const button = buttons[i]; + if (isChecked[i]) { + is( + button.getAttribute("aria-pressed"), + "true", + "The " + button.id + " button should set 'aria-pressed' = true." + ); + } else { + is( + button.getAttribute("aria-pressed"), + "false", + "The " + button.id + " button should set 'aria-pressed' = false." + ); + } + } +} + +/** + * Performs a single XMLHttpRequest and returns a promise that resolves once + * the request has loaded. + * + * @param Object data + * { method: the request method (default: "GET"), + * url: the url to request (default: content.location.href), + * body: the request body to send (default: ""), + * nocache: append an unique token to the query string (default: true), + * requestHeaders: set request headers (default: none) + * } + * + * @return Promise A promise that's resolved with object + * { status: XMLHttpRequest.status, + * response: XMLHttpRequest.response } + * + */ +function promiseXHR(data) { + return new Promise((resolve, reject) => { + const xhr = new content.XMLHttpRequest(); + + const method = data.method || "GET"; + let url = data.url || content.location.href; + const body = data.body || ""; + + if (data.nocache) { + url += "?devtools-cachebust=" + Math.random(); + } + + xhr.addEventListener( + "loadend", + function (event) { + resolve({ status: xhr.status, response: xhr.response }); + }, + { once: true } + ); + + xhr.open(method, url); + + // Set request headers + if (data.requestHeaders) { + data.requestHeaders.forEach(header => { + xhr.setRequestHeader(header.name, header.value); + }); + } + + xhr.send(body); + }); +} + +/** + * Performs a single websocket request and returns a promise that resolves once + * the request has loaded. + * + * @param Object data + * { url: the url to request (default: content.location.href), + * nocache: append an unique token to the query string (default: true), + * } + * + * @return Promise A promise that's resolved with object + * { status: websocket status(101), + * response: empty string } + * + */ +function promiseWS(data) { + return new Promise((resolve, reject) => { + let url = data.url; + + if (data.nocache) { + url += "?devtools-cachebust=" + Math.random(); + } + + /* Create websocket instance */ + const socket = new content.WebSocket(url); + + /* Since we only use HTTP server to mock websocket, so just ignore the error */ + socket.onclose = e => { + socket.close(); + resolve({ + status: 101, + response: "", + }); + }; + + socket.onerror = e => { + socket.close(); + resolve({ + status: 101, + response: "", + }); + }; + }); +} + +/** + * Perform the specified requests in the context of the page content. + * + * @param Array requests + * An array of objects specifying the requests to perform. See + * shared/test/frame-script-utils.js for more information. + * + * @return A promise that resolves once the requests complete. + */ +async function performRequestsInContent(requests) { + if (!Array.isArray(requests)) { + requests = [requests]; + } + + const responses = []; + + info("Performing requests in the context of the content."); + + for (const request of requests) { + const requestFn = request.ws ? promiseWS : promiseXHR; + const response = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [request], + requestFn + ); + responses.push(response); + } +} + +function testColumnsAlignment(headers, requestList) { + const firstRequestLine = requestList.childNodes[0]; + + // Find number of columns + const numberOfColumns = headers.childElementCount; + for (let i = 0; i < numberOfColumns; i++) { + const headerColumn = headers.childNodes[i]; + const requestColumn = firstRequestLine.childNodes[i]; + is( + headerColumn.getBoundingClientRect().left, + requestColumn.getBoundingClientRect().left, + "Headers for columns number " + i + " are aligned." + ); + } +} + +async function hideColumn(monitor, column) { + const { document } = monitor.panelWin; + + info(`Clicking context-menu item for ${column}`); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector(".requests-list-headers") + ); + + const onHeaderRemoved = waitForDOM( + document, + `#requests-list-${column}-button`, + 0 + ); + await selectContextMenuItem(monitor, `request-list-header-${column}-toggle`); + await onHeaderRemoved; + + ok( + !document.querySelector(`#requests-list-${column}-button`), + `Column ${column} should be hidden` + ); +} + +async function showColumn(monitor, column) { + const { document } = monitor.panelWin; + + info(`Clicking context-menu item for ${column}`); + EventUtils.sendMouseEvent( + { type: "contextmenu" }, + document.querySelector(".requests-list-headers") + ); + + const onHeaderAdded = waitForDOM( + document, + `#requests-list-${column}-button`, + 1 + ); + await selectContextMenuItem(monitor, `request-list-header-${column}-toggle`); + await onHeaderAdded; + + ok( + document.querySelector(`#requests-list-${column}-button`), + `Column ${column} should be visible` + ); +} + +/** + * Select a request and switch to its response panel. + * + * @param {Number} index The request index to be selected + */ +async function selectIndexAndWaitForSourceEditor(monitor, index) { + const { document } = monitor.panelWin; + const onResponseContent = monitor.panelWin.api.once( + TEST_EVENTS.RECEIVED_RESPONSE_CONTENT + ); + // Select the request first, as it may try to fetch whatever is the current request's + // responseContent if we select the ResponseTab first. + EventUtils.sendMouseEvent( + { type: "mousedown" }, + document.querySelectorAll(".request-list-item")[index] + ); + // We may already be on the ResponseTab, so only select it if needed. + const editor = document.querySelector("#response-panel .CodeMirror-code"); + if (!editor) { + const waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code"); + document.querySelector("#response-tab").click(); + await waitDOM; + } + await onResponseContent; +} + +/** + * Helper function for executing XHRs on a test page. + * + * @param {Number} count Number of requests to be executed. + */ +async function performRequests(monitor, tab, count) { + const wait = waitForNetworkEvents(monitor, count); + await ContentTask.spawn(tab.linkedBrowser, count, requestCount => { + content.wrappedJSObject.performRequests(requestCount); + }); + await wait; +} + +/** + * Helper function for retrieving `.CodeMirror` content + */ +function getCodeMirrorValue(monitor) { + const { document } = monitor.panelWin; + return document.querySelector(".CodeMirror").CodeMirror.getValue(); +} + +/** + * Helper function opening the options menu + */ +function openSettingsMenu(monitor) { + const { document } = monitor.panelWin; + document.querySelector(".netmonitor-settings-menu-button").click(); +} + +function clickSettingsMenuItem(monitor, itemKey) { + openSettingsMenu(monitor); + const node = getSettingsMenuItem(monitor, itemKey); + node.click(); +} + +function getSettingsMenuItem(monitor, itemKey) { + // The settings menu is injected into the toolbox document, + // so we must use the panelWin parent to query for items + const { parent } = monitor.panelWin; + const { document } = parent; + + return document.querySelector(SETTINGS_MENU_ITEMS[itemKey]); +} + +/** + * Wait for lazy fields to be loaded in a request. + * + * @param Object Store redux store containing request list. + * @param array fields array of strings which contain field names to be checked + * on the request. + */ +function waitForRequestData(store, fields, id) { + return waitUntil(() => { + let item; + if (id) { + item = getRequestById(store.getState(), id); + } else { + item = getSortedRequests(store.getState())[0]; + } + if (!item) { + return false; + } + for (const field of fields) { + if (!item[field]) { + return false; + } + } + return true; + }); +} + +// Telemetry + +/** + * Helper for verifying telemetry event. + * + * @param Object expectedEvent object representing expected event data. + * @param Object query fields specifying category, method and object + * of the target telemetry event. + */ +function checkTelemetryEvent(expectedEvent, query) { + const events = queryTelemetryEvents(query); + 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 logged event"); + + const f = e => JSON.stringify(e, null, 2); + is( + f(event), + f({ + ...expectedEvent, + session_id: event.session_id, + }), + "The event has the expected data" + ); +} + +function queryTelemetryEvents(query) { + const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS; + const snapshot = Services.telemetry.snapshotEvents(ALL_CHANNELS, true); + const category = query.category || "devtools.main"; + const object = query.object || "netmonitor"; + + const filtersChangedEvents = snapshot.parent.filter( + event => + event[1] === category && event[2] === query.method && event[3] === object + ); + + // Return the `extra` field (which is event[5]e). + return filtersChangedEvents.map(event => event[5]); +} +/** + * Check that the provided requests match the requests displayed in the netmonitor. + * + * @param {array} requests + * The expected requests. + * @param {object} monitor + * The netmonitor instance. + * @param {object=} options + * @param {boolean} allowDifferentOrder + * When set to true, requests are allowed to be in a different order in the + * netmonitor than in the expected requests array. Defaults to false. + */ +function validateRequests(requests, monitor, options = {}) { + const { allowDifferentOrder } = options; + const { document, store, windowRequire } = monitor.panelWin; + + const { getDisplayedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const sortedRequests = getSortedRequests(store.getState()); + + requests.forEach((spec, i) => { + const { method, url, causeType, causeUri, stack } = spec; + + let requestItem; + if (allowDifferentOrder) { + requestItem = sortedRequests.find(r => r.url === url); + } else { + requestItem = sortedRequests[i]; + } + + verifyRequestItemTarget( + document, + getDisplayedRequests(store.getState()), + requestItem, + method, + url, + { cause: { type: causeType, loadingDocumentUri: causeUri } } + ); + + const { stacktrace } = requestItem; + const stackLen = stacktrace ? stacktrace.length : 0; + + if (stack) { + ok(stacktrace, `Request #${i} has a stacktrace`); + Assert.greater( + stackLen, + 0, + `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items` + ); + + // if "stack" is array, check the details about the top stack frames + if (Array.isArray(stack)) { + stack.forEach((frame, j) => { + // If the `fn` is "*", it means the request is triggered from chrome + // resources, e.g. `resource:///modules/XX.jsm`, so we skip checking + // the function name for now (bug 1280266). + if (frame.file.startsWith("resource:///")) { + todo(false, "Requests from chrome resource should not be included"); + } else { + let value = stacktrace[j].functionName; + if (Object.is(value, null)) { + value = undefined; + } + is( + value, + frame.fn, + `Request #${i} has the correct function on JS stack frame #${j}` + ); + is( + stacktrace[j].filename.split("/").pop(), + frame.file.split("/").pop(), + `Request #${i} has the correct file on JS stack frame #${j}` + ); + is( + stacktrace[j].lineNumber, + frame.line, + `Request #${i} has the correct line number on JS stack frame #${j}` + ); + value = stacktrace[j].asyncCause; + if (Object.is(value, null)) { + value = undefined; + } + is( + value, + frame.asyncCause, + `Request #${i} has the correct async cause on JS stack frame #${j}` + ); + } + }); + } + } else { + is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`); + } + }); +} + +/** + * Retrieve the context menu element corresponding to the provided id, for the provided + * netmonitor instance. + * @param {Object} monitor + * The network monnitor object + * @param {String} id + * The id of the context menu item + */ +function getContextMenuItem(monitor, id) { + const Menu = require("resource://devtools/client/framework/menu.js"); + return Menu.getMenuElementById(id, monitor.panelWin.document); +} + +async function maybeOpenAncestorMenu(menuItem) { + const parentPopup = menuItem.parentNode; + if (parentPopup.state == "shown") { + return; + } + const shown = BrowserTestUtils.waitForEvent(parentPopup, "popupshown"); + if (parentPopup.state == "showing") { + await shown; + return; + } + const parentMenu = parentPopup.parentNode; + await maybeOpenAncestorMenu(parentMenu); + parentMenu.openMenu(true); + await shown; +} + +/* + * Selects and clicks the context menu item, it should + * also wait for the popup to close. + * @param {Object} monitor + * The network monnitor object + * @param {String} id + * The id of the context menu item + */ +async function selectContextMenuItem(monitor, id) { + const contextMenuItem = getContextMenuItem(monitor, id); + + const popup = contextMenuItem.parentNode; + await maybeOpenAncestorMenu(contextMenuItem); + const hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + popup.activateItem(contextMenuItem); + await hidden; +} + +/** + * Wait for DOM being in specific state. But, do not wait + * for change if it's in the expected state already. + */ +async function waitForDOMIfNeeded(target, selector, expectedLength = 1) { + return new Promise(resolve => { + const elements = target.querySelectorAll(selector); + if (elements.length == expectedLength) { + resolve(elements); + } else { + waitForDOM(target, selector, expectedLength).then(elems => { + resolve(elems); + }); + } + }); +} + +/** + * Helper for blocking or unblocking a request via the list item's context menu. + * + * @param {Element} element + * Target request list item to be right clicked to bring up its context menu. + * @param {Object} monitor + * The netmonitor instance used for retrieving a context menu element. + * @param {Object} store + * The redux store (wait-service middleware required). + * @param {String} action + * The action, block or unblock, to construct a corresponding context menu id. + */ +async function toggleBlockedUrl(element, monitor, store, action = "block") { + EventUtils.sendMouseEvent({ type: "contextmenu" }, element); + const contextMenuId = `request-list-context-${action}-url`; + const onRequestComplete = waitForDispatch( + store, + "REQUEST_BLOCKING_UPDATE_COMPLETE" + ); + await selectContextMenuItem(monitor, contextMenuId); + + info(`Wait for selected request to be ${action}ed`); + await onRequestComplete; + info(`Selected request is now ${action}ed`); +} + +/** + * Find and click an element + * + * @param {Element} element + * Target element to be clicked + * @param {Object} monitor + * The netmonitor instance used for retrieving the window. + */ + +function clickElement(element, monitor) { + EventUtils.synthesizeMouseAtCenter(element, {}, monitor.panelWin); +} + +/** + * Register a listener to be notified when a favicon finished loading and + * dispatch a "devtools:test:favicon" event to the favicon's link element. + * + * @param {Browser} browser + * Target browser to observe the favicon load. + */ +function registerFaviconNotifier(browser) { + const listener = async (name, data) => { + if (name == "SetIcon" || name == "SetFailedIcon") { + await SpecialPowers.spawn(browser, [], async () => { + content.document + .querySelector("link[rel='icon']") + .dispatchEvent(new content.CustomEvent("devtools:test:favicon")); + }); + LinkHandlerParent.removeListenerForTests(listener); + } + }; + LinkHandlerParent.addListenerForTests(listener); +} + +/** + * Predicates used when sorting items. + * + * @param object first + * The first item used in the comparison. + * @param object second + * The second item used in the comparison. + * @return number + * <0 to sort first to a lower index than second + * =0 to leave first and second unchanged with respect to each other + * >0 to sort second to a lower index than first + */ + +function compareValues(first, second) { + if (first === second) { + return 0; + } + return first > second ? 1 : -1; +} + +/** + * Click on the "Response" tab to open "Response" panel in the sidebar. + * @param {Document} doc + * Network panel document. + * @param {String} name + * Network panel sidebar tab name. + */ +const clickOnSidebarTab = (doc, name) => { + AccessibilityUtils.setEnv({ + // Keyboard accessibility is handled on the sidebar tabs container level + // (nav). Users can use arrow keys to navigate between and select tabs. + nonNegativeTabIndexRule: false, + }); + EventUtils.sendMouseEvent( + { type: "click" }, + doc.querySelector(`#${name}-tab`) + ); + AccessibilityUtils.resetEnv(); +}; + +/** + * Add a new blocked request URL pattern. The request blocking sidepanel should + * already be opened. + * + * @param {string} pattern + * The URL pattern to add to block requests. + * @param {Object} monitor + * The netmonitor instance. + */ +async function addBlockedRequest(pattern, monitor) { + info("Add a blocked request for the URL pattern " + pattern); + const doc = monitor.panelWin.document; + + const addRequestForm = await waitFor(() => + doc.querySelector( + "#network-action-bar-blocked-panel .request-blocking-add-form" + ) + ); + ok(!!addRequestForm, "The request blocking side panel is not available"); + + info("Wait for the add input to get focus"); + await waitFor(() => + addRequestForm.querySelector("input.devtools-searchinput:focus") + ); + + typeInNetmonitor(pattern, monitor); + EventUtils.synthesizeKey("KEY_Enter"); +} + +/** + * Check if the provided .request-list-item element corresponds to a blocked + * request. + * + * @param {Element} + * The request's DOM element. + * @returns {boolean} + * True if the request is displayed as blocked, false otherwise. + */ +function checkRequestListItemBlocked(item) { + return item.className.includes("blocked"); +} + +/** + * Type the provided string the netmonitor window. The correct input should be + * focused prior to using this helper. + * + * @param {string} string + * The string to type. + * @param {Object} monitor + * The netmonitor instance used to type the string. + */ +function typeInNetmonitor(string, monitor) { + for (const ch of string) { + EventUtils.synthesizeKey(ch, {}, monitor.panelWin); + } +} + +/** + * Opens/ closes the URL preview in the headers side panel + * + * @param {Boolean} shouldExpand + * @param {NetMonitorPanel} monitor + * @returns + */ +async function toggleUrlPreview(shouldExpand, monitor) { + const { document } = monitor.panelWin; + const wait = waitUntil(() => { + const rowSize = document.querySelectorAll( + "#headers-panel .url-preview tr.treeRow" + ).length; + return shouldExpand ? rowSize > 1 : rowSize == 1; + }); + + clickElement( + document.querySelector( + "#headers-panel .url-preview tr:first-child span.treeIcon.theme-twisty" + ), + monitor + ); + return wait; +} + +/** + * Wait for the eager evaluated result from the split console + * @param {Object} hud + * @param {String} text - expected evaluation result + */ +async function waitForEagerEvaluationResult(hud, text) { + await waitUntil(() => { + const elem = hud.ui.outputNode.querySelector(".eager-evaluation-result"); + if (elem) { + if (text instanceof RegExp) { + return text.test(elem.innerText); + } + return elem.innerText == text; + } + return false; + }); + ok(true, `Got eager evaluation result ${text}`); +} diff --git a/devtools/client/netmonitor/test/html_api-calls-test-page.html b/devtools/client/netmonitor/test/html_api-calls-test-page.html new file mode 100644 index 0000000000..1385101a64 --- /dev/null +++ b/devtools/client/netmonitor/test/html_api-calls-test-page.html @@ -0,0 +1,45 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>API calls request test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(); + }); + } + + async function performRequests() { + await get("/api/fileName.xml"); + await get("/api/file%E2%98%A2.xml"); + await get("/api/ascii/get/"); + await get("/api/unicode/%E2%98%A2/"); + await get("/api/search/?q=search%E2%98%A2"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_brotli-test-page.html b/devtools/client/netmonitor/test/html_brotli-test-page.html new file mode 100644 index 0000000000..7b3f01c4d2 --- /dev/null +++ b/devtools/client/netmonitor/test/html_brotli-test-page.html @@ -0,0 +1,41 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Brotli test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=br"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_cache-test-page.html b/devtools/client/netmonitor/test/html_cache-test-page.html new file mode 100644 index 0000000000..fa71a415f6 --- /dev/null +++ b/devtools/client/netmonitor/test/html_cache-test-page.html @@ -0,0 +1,28 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor cache test page</title> + </head> + + <body> + <p>Cache test</p> + + <script type="text/javascript"> + "use strict"; + + window.sendRequestWithStatus = status => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", `sjs_content-type-test-server.sjs?sts=${status}&fmt=html`, true); + xhr.send(null); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_cause-test-page.html b/devtools/client/netmonitor/test/html_cause-test-page.html new file mode 100644 index 0000000000..4ec79936e4 --- /dev/null +++ b/devtools/client/netmonitor/test/html_cause-test-page.html @@ -0,0 +1,98 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + <link rel="stylesheet" type="text/css" href="stylesheet_request" /> + </head> + + <body> + <p>Request cause test</p> + <img src="img_request" /> + <img srcset="img_srcset_request" /> + <!-- ensure that the two next <img> are offscreen + so that the error listener is registered before we try loading them --> + <div style="height: 2000vh;"></div> + <img loading="lazy" src="lazy_img_request" /> + <img loading="lazy" srcset="lazy_img_srcset_request" /> + <script type="text/javascript"> + "use strict"; + + function performXhrRequest() { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "xhr_request", true); + return new Promise(function performXhrRequestCallback(resolve) { + xhr.onload = resolve; + xhr.send(); + }); + } + + function performFetchRequest() { + return fetch("fetch_request"); + } + + // Perform some requests with async stacks + function performPromiseFetchRequest() { + return Promise.resolve().then(function performPromiseFetchRequestCallback() { + return fetch("promise_fetch_request"); + }); + } + + function performTimeoutFetchRequest() { + return new Promise(function performTimeoutFetchRequestCallback1(resolve) { + setTimeout(function performTimeoutFetchRequestCallback2() { + resolve(fetch("timeout_fetch_request")); + }, 0); + }); + } + + function performFavicon() { + return new Promise(function (resolve) { + const link = document.createElement("link"); + link.rel = "icon"; + link.href = "favicon_request"; + document.querySelector("head").appendChild(link); + link.addEventListener("devtools:test:favicon", resolve); + }); + } + + function performLazyLoadingImage() { + return new Promise(function (resolve) { + const lazyImgs = document.querySelectorAll("img[loading='lazy']"); + + const promises = [ + new Promise(r => lazyImgs[0].addEventListener("error", r)), + new Promise(r => lazyImgs[1].addEventListener("error", r)), + ]; + + // Given that the default display style of <img> is `inline` so + // it's sufficient to scroll to an <img>. + lazyImgs[0].scrollIntoView({ behavior: "instant" }); + resolve(Promise.all(promises)); + }); + } + + function performBeaconRequest() { + navigator.sendBeacon("beacon_request"); + } + + (async function() { + await performXhrRequest(); + await performFetchRequest(); + await performPromiseFetchRequest(); + await performTimeoutFetchRequest(); + await performFavicon(); + await performLazyLoadingImage(); + + // Finally, send a beacon request + performBeaconRequest(); + })(); + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html b/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html new file mode 100644 index 0000000000..af27ab0835 --- /dev/null +++ b/devtools/client/netmonitor/test/html_content-type-without-cache-test-page.html @@ -0,0 +1,47 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Content type test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=xml"); + await get("sjs_content-type-test-server.sjs?fmt=css"); + await get("sjs_content-type-test-server.sjs?fmt=js"); + await get("sjs_content-type-test-server.sjs?fmt=json"); + await get("sjs_content-type-test-server.sjs?fmt=bogus"); + await get("test-image.png?v=" + Math.random()); + await get("sjs_content-type-test-server.sjs?fmt=gzip"); + await get("sjs_content-type-test-server.sjs?fmt=br"); + } + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_copy-as-curl.html b/devtools/client/netmonitor/test/html_copy-as-curl.html new file mode 100644 index 0000000000..87930954ab --- /dev/null +++ b/devtools/client/netmonitor/test/html_copy-as-curl.html @@ -0,0 +1,33 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Performing requests of various methods, with or without payload.</p> + + <script type="text/javascript"> + /* exported performRequest */ + "use strict"; + + function performRequest(url, method, payload = null) { + const xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + xhr.setRequestHeader("Accept-Language", window.navigator.language); + xhr.setRequestHeader("X-Custom-Header-1", "Custom value"); + xhr.setRequestHeader("X-Custom-Header-2", "8.8.8.8"); + xhr.setRequestHeader("X-Custom-Header-3", "Mon, 3 Mar 2014 11:11:11 GMT"); + xhr.send(payload); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_cors-test-page.html b/devtools/client/netmonitor/test/html_cors-test-page.html new file mode 100644 index 0000000000..9ad1f82344 --- /dev/null +++ b/devtools/client/netmonitor/test/html_cors-test-page.html @@ -0,0 +1,34 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>POST with CORS test page</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function post(url, contentType, postData) { + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", contentType); + xhr.send(postData); + } + + function performRequests(url, contentType, postData) { + post(url, contentType, postData); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_csp-frame-test-page.html b/devtools/client/netmonitor/test/html_csp-frame-test-page.html new file mode 100644 index 0000000000..7ef4e23825 --- /dev/null +++ b/devtools/client/netmonitor/test/html_csp-frame-test-page.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset=utf-8> + </head> + <body> + <iframe src="https://example.org/browser/devtools/client/netmonitor/test/html_csp-test-page.html" width="50%" height="50%"></iframe> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_csp-resend-test-page.html b/devtools/client/netmonitor/test/html_csp-resend-test-page.html new file mode 100644 index 0000000000..f6ec955a32 --- /dev/null +++ b/devtools/client/netmonitor/test/html_csp-resend-test-page.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset=utf-8> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src *; script-src 'unsafe-inline'"> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Tests 'Edit and Resend' requests re-uses the same content-type</title> + </head> + <body> + The image will be allowed to load by the CSP.<br/> + <img id="test-img"></img><br/> + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function performRequests() { + const testImg = document.getElementById("test-img"); + testImg.src = "test-image.png"; + } + + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_csp-test-page.html b/devtools/client/netmonitor/test/html_csp-test-page.html new file mode 100644 index 0000000000..2329f4fa1e --- /dev/null +++ b/devtools/client/netmonitor/test/html_csp-test-page.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset=utf-8> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <meta http-equiv="Content-Security-Policy" content="script-src 'none'; style-src 'none';"> + <title>Tests breaking CSP with script</title> + <link href="internal-loaded.css" rel="stylesheet" type="text/css"> + </head> + <body> + + The script in this page will CSP: + + <script src="js_websocket-worker-test.js"></script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_curl-utils.html b/devtools/client/netmonitor/test/html_curl-utils.html new file mode 100644 index 0000000000..bd09a63ba5 --- /dev/null +++ b/devtools/client/netmonitor/test/html_curl-utils.html @@ -0,0 +1,136 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Performing requests</p> + + <p> + <canvas width="100" height="100"></canvas> + </p> + + <hr/> + + <form method="post" action="#" enctype="multipart/form-data" target="target" id="post-form"> + <input type="text" name="param1" value="value1"/> + <input type="text" name="param2" value="value2"/> + <input type="text" name="param3" value="value3"/> + <input type="submit"/> + </form> + <iframe name="target"></iframe> + + <script type="text/javascript"> + /* exported performRequests */ + /* eslint-disable max-nested-callbacks */ + "use strict"; + + function ajaxGet(url, callback) { + const xhr = new XMLHttpRequest(); + xhr.open("GET", url + "?param1=value1¶m2=value2¶m3=value3", true); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.onload = function() { + callback(); + }; + xhr.send(); + } + + function ajaxPost(url, callback) { + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.onload = function() { + callback(); + }; + const params = "param1=value1¶m2=value2¶m3=value3"; + xhr.send(params); + } + + function ajaxPostJson(url, callback) { + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.onload = function() { + callback(); + }; + const params = { + param1: "value1", + param2: "value2", + }; + const jsonParams = JSON.stringify(params); + xhr.send(jsonParams); + } + + function ajaxPatch(url, callback) { + const xhr = new XMLHttpRequest(); + xhr.open("PATCH", url, true); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.onload = function() { + callback(); + }; + const params = "param1=value1¶m2=value2¶m3=value3"; + xhr.send(params); + } + + function ajaxMultipart(url, callback) { + const xhr = new XMLHttpRequest(); + xhr.open("POST", url, true); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.onload = function() { + callback(); + }; + + getCanvasElem().toBlob((blob) => { + const formData = new FormData(); + formData.append("param1", "value1"); + formData.append("file", blob, "filename.png"); + xhr.send(formData); + }); + } + + function submitForm() { + const form = document.querySelector("#post-form"); + form.submit(); + } + + function getCanvasElem() { + return document.querySelector("canvas"); + } + + function initCanvas() { + const canvas = getCanvasElem(); + const ctx = canvas.getContext("2d"); + ctx.fillRect(0, 0, 100, 100); + ctx.clearRect(20, 20, 60, 60); + ctx.strokeRect(25, 25, 50, 50); + } + + function performRequests(url) { + ajaxGet(url, () => { + ajaxPost(url, () => { + ajaxPostJson(url, () => { + ajaxPatch(url, () => { + ajaxMultipart(url, () => { + submitForm(); + }); + }); + }); + }); + }); + } + + initCanvas(); + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_custom-get-page.html b/devtools/client/netmonitor/test/html_custom-get-page.html new file mode 100644 index 0000000000..b44bf754a4 --- /dev/null +++ b/devtools/client/netmonitor/test/html_custom-get-page.html @@ -0,0 +1,58 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Performing a custom number of GETs</p> + + <script type="text/javascript"> + /* exported performRequests hasOfflineEventFired */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + // Use a count parameter to defeat caching. + let count = 0; + + async function performRequests(total, url, timeout = 0) { + if (!total) { + return; + } + await get(url || "request_" + (count++)); + setTimeout(performRequests.bind(this, --total, url, timeout), timeout); + } + + // For testing the offline mode in the netmonitor + let isOfflineEventFired = false; + window.addEventListener("offline", (event) => { + isOfflineEventFired = true + }, { once: true }); + + function hasOfflineEventFired() { + return isOfflineEventFired; + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_cyrillic-test-page.html b/devtools/client/netmonitor/test/html_cyrillic-test-page.html new file mode 100644 index 0000000000..309edb473b --- /dev/null +++ b/devtools/client/netmonitor/test/html_cyrillic-test-page.html @@ -0,0 +1,42 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Cyrillic type test</p> + <p>Братан, ты вообще качаешься?</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=txt"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_filter-test-page.html b/devtools/client/netmonitor/test/html_filter-test-page.html new file mode 100644 index 0000000000..b0bd48301f --- /dev/null +++ b/devtools/client/netmonitor/test/html_filter-test-page.html @@ -0,0 +1,58 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Filtering test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + // Use a random parameter to defeat caching. + xhr.open("GET", address + "&" + Math.random(), true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests(optionsText) { + const options = JSON.parse(optionsText); + + await get("sjs_content-type-test-server.sjs?fmt=html&res=" + options.htmlContent); + await get("sjs_content-type-test-server.sjs?fmt=css"); + await get("sjs_content-type-test-server.sjs?fmt=js"); + await get("sjs_content-type-test-server.sjs?fmt=xhtml"); + if (!options.getMedia) { + return; + } + await get("sjs_content-type-test-server.sjs?fmt=font"); + await get("sjs_content-type-test-server.sjs?fmt=image"); + await get("sjs_content-type-test-server.sjs?fmt=audio"); + await get("sjs_content-type-test-server.sjs?fmt=video"); + if (!options.getFlash) { + return; + } + await get("sjs_content-type-test-server.sjs?fmt=flash"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_fonts-test-page.html b/devtools/client/netmonitor/test/html_fonts-test-page.html new file mode 100644 index 0000000000..c68b35f08d --- /dev/null +++ b/devtools/client/netmonitor/test/html_fonts-test-page.html @@ -0,0 +1,45 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + <style> + @font-face { + font-family: test-font; + src: url("ostrich-regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; + } + + @font-face { + font-family: test-font-bold; + src: url("ostrich-black.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; + } + + .regular { + font-family: test-font; + } + + .bold { + font-family: test-font-bold; + font-weight: 700; + } + </style> + </head> + + <body> + <p class="regular">Regular font</p> + <p class="bold">Bold font</p> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_frame-subdocument.html b/devtools/client/netmonitor/test/html_frame-subdocument.html new file mode 100644 index 0000000000..84a160a266 --- /dev/null +++ b/devtools/client/netmonitor/test/html_frame-subdocument.html @@ -0,0 +1,50 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + <link rel="stylesheet" type="text/css" href="stylesheet_request" /> + </head> + + <body> + <p>Request frame test</p> + <img src="img_request" /> + <script type="text/javascript"> + "use strict"; + + function performXhrRequest() { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "xhr_request", true); + xhr.send(); + } + + function performFetchRequest() { + fetch("fetch_request"); + } + + function performBeaconRequest() { + navigator.sendBeacon("beacon_request"); + } + + performXhrRequest(); + performFetchRequest(); + + // Perform some requests with async stacks + Promise.resolve().then(function performPromiseFetchRequest() { + fetch("promise_fetch_request"); + setTimeout(function performTimeoutFetchRequest() { + fetch("timeout_fetch_request"); + + // Finally, send a beacon request + performBeaconRequest(); + }, 0); + }); + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_frame-test-page.html b/devtools/client/netmonitor/test/html_frame-test-page.html new file mode 100644 index 0000000000..edf251ada3 --- /dev/null +++ b/devtools/client/netmonitor/test/html_frame-test-page.html @@ -0,0 +1,51 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + <link rel="stylesheet" type="text/css" href="stylesheet_request" /> + </head> + + <body> + <p>Request frame test</p> + <img src="img_request" /> + <iframe src="html_frame-subdocument.html"></iframe> + <script type="text/javascript"> + "use strict"; + + function performXhrRequest() { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "xhr_request", true); + xhr.send(); + } + + function performFetchRequest() { + fetch("fetch_request"); + } + + function performBeaconRequest() { + navigator.sendBeacon("beacon_request"); + } + + performXhrRequest(); + performFetchRequest(); + + // Perform some requests with async stacks + Promise.resolve().then(function performPromiseFetchRequest() { + fetch("promise_fetch_request"); + setTimeout(function performTimeoutFetchRequest() { + fetch("timeout_fetch_request"); + + // Finally, send a beacon request + performBeaconRequest(); + }, 0); + }); + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_header-test-page.html b/devtools/client/netmonitor/test/html_header-test-page.html new file mode 100644 index 0000000000..c0d9a85bdc --- /dev/null +++ b/devtools/client/netmonitor/test/html_header-test-page.html @@ -0,0 +1,43 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>POST raw test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function post(address, message, callback) { + const xhr = new XMLHttpRequest(); + xhr.open("POST", address, true); + xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded"); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + callback(); + } + }; + xhr.send(message); + } + + function performRequests() { + const rawData = ""; + post("sjs_simple-test-server.sjs?file=foo+%23+bar#home", rawData, function() { + // Done. + }); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_image-cache.html b/devtools/client/netmonitor/test/html_image-cache.html new file mode 100644 index 0000000000..b36e388755 --- /dev/null +++ b/devtools/client/netmonitor/test/html_image-cache.html @@ -0,0 +1,7 @@ +<html> + <body> + <img src="test-image.png"> + <img src="test-image.png"> + <img src="test-image.png"> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_image-tooltip-test-page.html b/devtools/client/netmonitor/test/html_image-tooltip-test-page.html new file mode 100644 index 0000000000..299a75a1ab --- /dev/null +++ b/devtools/client/netmonitor/test/html_image-tooltip-test-page.html @@ -0,0 +1,29 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>tooltip test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function performRequests() { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "test-image.png?v=" + Math.random(), true); + xhr.send(null); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_infinite-get-page.html b/devtools/client/netmonitor/test/html_infinite-get-page.html new file mode 100644 index 0000000000..462627e81d --- /dev/null +++ b/devtools/client/netmonitor/test/html_infinite-get-page.html @@ -0,0 +1,50 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Infinite GETs</p> + + <script type="text/javascript"> + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + // Use a count parameter to defeat caching. + let count = 0; + let doRequests = true; + function stopRequests() { // eslint-disable-line no-unused-vars + doRequests = false; + } + + (async function performRequests() { + await get("request_" + (count++)); + if (doRequests) { + setTimeout(performRequests, 50); + } + })(); + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_internal-stylesheet.html b/devtools/client/netmonitor/test/html_internal-stylesheet.html new file mode 100644 index 0000000000..a3add17520 --- /dev/null +++ b/devtools/client/netmonitor/test/html_internal-stylesheet.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> +"use strict"; + +const html = ` + <!DOCTYPE html> + <head> + <link rel="stylesheet" href="internal-loaded.css"> + </head> +`; +onload = function() { + document.write(html); +} +</script> diff --git a/devtools/client/netmonitor/test/html_json-b64.html b/devtools/client/netmonitor/test/html_json-b64.html new file mode 100644 index 0000000000..dd76b74e36 --- /dev/null +++ b/devtools/client/netmonitor/test/html_json-b64.html @@ -0,0 +1,41 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>JSON b64 test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=json-b64"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_json-basic.html b/devtools/client/netmonitor/test/html_json-basic.html new file mode 100644 index 0000000000..2f48d8fab6 --- /dev/null +++ b/devtools/client/netmonitor/test/html_json-basic.html @@ -0,0 +1,43 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>JSON request test page</p> + <p>Pass the JSON name (as supported by sjs_json-test-server.sjs) as a query parameter</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + // Forward the query parameter for this page to sjs_json-test-server + await get("sjs_json-test-server.sjs" + window.location.search); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html b/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html new file mode 100644 index 0000000000..c203f94dff --- /dev/null +++ b/devtools/client/netmonitor/test/html_json-custom-mime-test-page.html @@ -0,0 +1,41 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>JSONP test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=json-custom-mime"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_json-empty.html b/devtools/client/netmonitor/test/html_json-empty.html new file mode 100644 index 0000000000..99e743a4bb --- /dev/null +++ b/devtools/client/netmonitor/test/html_json-empty.html @@ -0,0 +1,42 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Empty JSON test page</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + // Forward the query parameter for this page to sjs_json-test-server + await get("sjs_json-test-server.sjs" + window.location.search); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_json-long-test-page.html b/devtools/client/netmonitor/test/html_json-long-test-page.html new file mode 100644 index 0000000000..afa9f47079 --- /dev/null +++ b/devtools/client/netmonitor/test/html_json-long-test-page.html @@ -0,0 +1,41 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>JSON long string test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=json-long"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_json-malformed-test-page.html b/devtools/client/netmonitor/test/html_json-malformed-test-page.html new file mode 100644 index 0000000000..61966a454e --- /dev/null +++ b/devtools/client/netmonitor/test/html_json-malformed-test-page.html @@ -0,0 +1,41 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>JSON malformed test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=json-malformed"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_json-text-mime-test-page.html b/devtools/client/netmonitor/test/html_json-text-mime-test-page.html new file mode 100644 index 0000000000..98d42a9450 --- /dev/null +++ b/devtools/client/netmonitor/test/html_json-text-mime-test-page.html @@ -0,0 +1,41 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>JSON text test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=json-text-mime"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_json-xssi-protection.html b/devtools/client/netmonitor/test/html_json-xssi-protection.html new file mode 100644 index 0000000000..2daf1725b8 --- /dev/null +++ b/devtools/client/netmonitor/test/html_json-xssi-protection.html @@ -0,0 +1,42 @@ + +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>JSON XSSI protection test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise (resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=json-valid-xssi-protection"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_jsonp-test-page.html b/devtools/client/netmonitor/test/html_jsonp-test-page.html new file mode 100644 index 0000000000..d30de40c06 --- /dev/null +++ b/devtools/client/netmonitor/test/html_jsonp-test-page.html @@ -0,0 +1,42 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>JSONP test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_content-type-test-server.sjs?fmt=jsonp&jsonp=$_0123Fun"); + await get("sjs_content-type-test-server.sjs?fmt=jsonp2&jsonp=$_4567Sad"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_maps-test-page.html b/devtools/client/netmonitor/test/html_maps-test-page.html new file mode 100644 index 0000000000..401d472e73 --- /dev/null +++ b/devtools/client/netmonitor/test/html_maps-test-page.html @@ -0,0 +1,24 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor source maps test page</title> + <link rel="stylesheet" type="text/css" href="stylesheet_request" /> + </head> + + <body> + <script type="text/javascript" src="xhr_bundle.js" charset="utf-8"></script> + <script type="text/javascript"> + "use strict"; + + /* globals doxhr */ + doxhr(); + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_navigate-test-page.html b/devtools/client/netmonitor/test/html_navigate-test-page.html new file mode 100644 index 0000000000..23f00f3dfd --- /dev/null +++ b/devtools/client/netmonitor/test/html_navigate-test-page.html @@ -0,0 +1,18 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Navigation test</p> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_open-request-in-tab.html b/devtools/client/netmonitor/test/html_open-request-in-tab.html new file mode 100644 index 0000000000..dd399c4364 --- /dev/null +++ b/devtools/client/netmonitor/test/html_open-request-in-tab.html @@ -0,0 +1,33 @@ +<!-- Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Performing a GET or POST request</p> + + <script type="text/javascript"> + /* exported performRequest */ + "use strict"; + + function performRequest(method, contentType, payload) { + const xhr = new XMLHttpRequest(); + const url = "sjs_method-test-server.sjs"; + xhr.open(method, url, true); + xhr.setRequestHeader("Accept-Language", window.navigator.language); + if (contentType) { + xhr.setRequestHeader("Content-Type", contentType); + } + xhr.send(payload || ""); + } + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_params-test-page.html b/devtools/client/netmonitor/test/html_params-test-page.html new file mode 100644 index 0000000000..3d657a5e87 --- /dev/null +++ b/devtools/client/netmonitor/test/html_params-test-page.html @@ -0,0 +1,78 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Request params type test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + async function get(address, query) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address + query, true); + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(); + }); + } + + async function request(address, query, contentType, requestBody, method) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open(method, address + query, true); + xhr.setRequestHeader("content-type", contentType); + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(requestBody); + }); + } + + async function post(address, query, contentType, postBody) { + return request(address, query, contentType, postBody, "POST"); + } + + async function patch(address, query, contentType, patchBody) { + return request(address, query, contentType, patchBody, "PATCH"); + } + + async function put(address, query, contentType, putBody) { + return request(address, query, contentType, putBody, "PUT"); + } + + async function performRequests() { + const urlencoded = "application/x-www-form-urlencoded"; + await post("baz", "?a", urlencoded, '{ "foo": "bar" }'); + await post("baz", "?a=b", urlencoded, '{ "foo": "bar" }'); + await post("baz", "?a=b", urlencoded, "?foo=bar=123=xyz"); + await post("baz", "?a", undefined, '{ "foo": "bar" }'); + await post("baz", "?a=b", undefined, '{ "foo": "bar" }'); + await post("baz", "?a=b", undefined, "?foo=bar"); + await get("baz", ""); + await patch("baz", "?a=b", urlencoded, '{ "foo": "bar" }'); + await put("baz", "?a=b", urlencoded, '{ "foo": "bar" }'); + await get("baz", "?species=in=(52,60)"); + await get("baz", "?a=&a=b"); + await get("baz", "?a=b&a=c&d=1"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_pause-test-page.html b/devtools/client/netmonitor/test/html_pause-test-page.html new file mode 100644 index 0000000000..a4ed668ce8 --- /dev/null +++ b/devtools/client/netmonitor/test/html_pause-test-page.html @@ -0,0 +1,36 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Performing a custom number of GETs</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function performRequests(url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_post-array-data-test-page.html b/devtools/client/netmonitor/test/html_post-array-data-test-page.html new file mode 100644 index 0000000000..386b043d1a --- /dev/null +++ b/devtools/client/netmonitor/test/html_post-array-data-test-page.html @@ -0,0 +1,34 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function performRequests() { + const xhr = new XMLHttpRequest(); + xhr.open("POST", "sjs_simple-test-server.sjs", true); + + const postData = JSON.stringify({ + watches: ["hello", "how", "are", "you", { + a: 10, + c: 15, + b: ["a", "c", "b"], + }], + }); + xhr.send(postData); + } + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_post-data-test-page.html b/devtools/client/netmonitor/test/html_post-data-test-page.html new file mode 100644 index 0000000000..b53e8b60e7 --- /dev/null +++ b/devtools/client/netmonitor/test/html_post-data-test-page.html @@ -0,0 +1,82 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + <style> + input { + display: block; + margin: 12px; + } + </style> + </head> + + <body> + <p>POST data test</p> + <form enctype="multipart/form-data" method="post" name="form-name"> + <input type="text" name="text" placeholder="text" value="Some text..."/> + <input type="email" name="email" placeholder="email"/> + <input type="range" name="range" value="42"/> + <input type="button" value="Post me!" onclick="window.form()"> + </form> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function post(address, message) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("POST", address, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + + let data = ""; + for (const i in message) { + data += "&" + i + "=" + encodeURIComponent(message[i]); + } + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(data); + }); + } + + function form(address, formName) { + return new Promise(resolve => { + const formData = new FormData(document.forms.namedItem(formName)); + formData.append("Custom field", "Extra data"); + + const xhr = new XMLHttpRequest(); + xhr.open("POST", address, true); + xhr.setRequestHeader("custom-header-xxx", "custom-value-xxx"); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(formData); + }); + } + + async function performRequests() { + const url = "sjs_simple-test-server.sjs"; + const url1 = url + "?foo=bar&baz=42&valueWithEqualSign=hijk=123=mnop&type=urlencoded"; + const url2 = url + "?foo=bar&baz=42&type=multipart"; + + await post(url1, { foo: "bar", baz: 123, valueWithEqualSign: "xyz=abc=123", valueWithAmpersand: "abcd&1234" }); + await form(url2, "form-name"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_post-json-test-page.html b/devtools/client/netmonitor/test/html_post-json-test-page.html new file mode 100644 index 0000000000..8c18e91dbb --- /dev/null +++ b/devtools/client/netmonitor/test/html_post-json-test-page.html @@ -0,0 +1,48 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>POST raw test</p> + + <script type="text/javascript"> + /* exported performRequests performLargePostDataRequest */ + "use strict"; + + function post(address, message, callback) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("POST", address, true); + xhr.setRequestHeader("Content-Type", "application/json"); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(message); + }); + } + + async function performRequests() { + await post("sjs_simple-test-server.sjs", JSON.stringify({a: 1})); + } + + async function performLargePostDataRequest() { + const limit = 1048576; + const data = "x".repeat(2 * limit); + await post("sjs_simple-test-server.sjs", JSON.stringify(data)); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_post-raw-test-page.html b/devtools/client/netmonitor/test/html_post-raw-test-page.html new file mode 100644 index 0000000000..190d384042 --- /dev/null +++ b/devtools/client/netmonitor/test/html_post-raw-test-page.html @@ -0,0 +1,43 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>POST raw test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function post(address, message) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("POST", address, true); + xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded"); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(message); + }); + } + + async function performRequests() { + const rawData = "foo=bar&baz=123"; + await post("sjs_simple-test-server.sjs", rawData); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html b/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html new file mode 100644 index 0000000000..1ff853edea --- /dev/null +++ b/devtools/client/netmonitor/test/html_post-raw-with-headers-test-page.html @@ -0,0 +1,67 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>POST raw with headers test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function post(address, message) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("POST", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(message); + }); + } + + async function performRequests() { + const rawData = [ + // Only one header + [ + "content-type: application/x-www-form-urlencoded\r", + "\r", + "\r", + "foo=bar&baz=123", + ], + // No form body content + [ + "content-type: application/x-www-form-urlencoded\r", + "\r", + "\r", + ], + // Multiple headers + [ + "content-type: application/x-www-form-urlencoded\r", + "custom-header: hello world!\r", + "\r", + "\r", + "foo=bar&baz=123", + ], + ]; + + for (const data of rawData) { + await post("sjs_simple-test-server.sjs", data.join("\n")); + } + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_send-beacon.html b/devtools/client/netmonitor/test/html_send-beacon.html new file mode 100644 index 0000000000..12ab8737d2 --- /dev/null +++ b/devtools/client/netmonitor/test/html_send-beacon.html @@ -0,0 +1,26 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Send beacon test</p> + + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function performRequests() { + navigator.sendBeacon("beacon_request"); + } + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_simple-test-page.html b/devtools/client/netmonitor/test/html_simple-test-page.html new file mode 100644 index 0000000000..846681dbd9 --- /dev/null +++ b/devtools/client/netmonitor/test/html_simple-test-page.html @@ -0,0 +1,18 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Simple test</p> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_single-get-page.html b/devtools/client/netmonitor/test/html_single-get-page.html new file mode 100644 index 0000000000..79c4b13e81 --- /dev/null +++ b/devtools/client/netmonitor/test/html_single-get-page.html @@ -0,0 +1,40 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Performing a custom number of GETs</p> + + <script type="text/javascript"> + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + (async function performRequests() { + await get("request_0"); + })(); + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_slow-requests-test-page.html b/devtools/client/netmonitor/test/html_slow-requests-test-page.html new file mode 100644 index 0000000000..93941d2705 --- /dev/null +++ b/devtools/client/netmonitor/test/html_slow-requests-test-page.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>Slow Network Requests Test Page</title> + </head> + <body> + <h1>Slow Network Requests Test Page</h1> + <script type="text/javascript" + src="http://example.com/browser/devtools/client/netmonitor/test/sjs_slow-script-server.sjs"></script> + </body> + </html> diff --git a/devtools/client/netmonitor/test/html_sorting-test-page.html b/devtools/client/netmonitor/test/html_sorting-test-page.html new file mode 100644 index 0000000000..640c58b8e8 --- /dev/null +++ b/devtools/client/netmonitor/test/html_sorting-test-page.html @@ -0,0 +1,18 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Sorting test</p> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_sse-test-page.html b/devtools/client/netmonitor/test/html_sse-test-page.html new file mode 100644 index 0000000000..bb726d4c1f --- /dev/null +++ b/devtools/client/netmonitor/test/html_sse-test-page.html @@ -0,0 +1,31 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype HTML> + +<html> +<head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>SSE Inspection Test Page</title> +</head> +<body> + <h1>SSE Inspection Test Page</h1> + <script type="text/javascript"> + /* exported openConnection */ + "use strict"; + + let es; + function openConnection() { + return new Promise(resolve => { + es = new EventSource("sjs_sse-test-server.sjs"); + es.onmessage = function (e) { + es.close(); + resolve(); + }; + }); + } + </script> +</body> +</html> diff --git a/devtools/client/netmonitor/test/html_statistics-edge-case-page.html b/devtools/client/netmonitor/test/html_statistics-edge-case-page.html new file mode 100644 index 0000000000..2355639008 --- /dev/null +++ b/devtools/client/netmonitor/test/html_statistics-edge-case-page.html @@ -0,0 +1,23 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <meta charset="utf-8"/> + <title>Test page with slow requests and image-cache requests</title> + </head> + <body> + <script type="text/javascript"> + "use strict"; + /* exported unblock */ + + // Open a request which will not resolve until unblock() is called. + fetch("sjs_long-polling-server.sjs").then(res => res.text()).then(console.log); + + function unblock() { + fetch("sjs_long-polling-server.sjs?unblock"); + } + </script> + <img src="test-image.png"> + <img src="test-image.png"> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_statistics-test-page.html b/devtools/client/netmonitor/test/html_statistics-test-page.html new file mode 100644 index 0000000000..a8afabe975 --- /dev/null +++ b/devtools/client/netmonitor/test/html_statistics-test-page.html @@ -0,0 +1,42 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Statistics test</p> + + <script type="text/javascript"> + "use strict"; + + function get(address) { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + xhr.send(null); + } + + get("sjs_content-type-test-server.sjs?sts=304&fmt=txt"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=xml"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=html"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=css"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=js"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=json"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=jsonp"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=font"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=image"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=audio"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=video"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=flash"); + get("test-image.png?v=" + Math.random()); + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_status-codes-test-page.html b/devtools/client/netmonitor/test/html_status-codes-test-page.html new file mode 100644 index 0000000000..99b4e2a22f --- /dev/null +++ b/devtools/client/netmonitor/test/html_status-codes-test-page.html @@ -0,0 +1,55 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Status codes test</p> + + <script type="text/javascript"> + /* exported performRequests, performCachedRequests, performOneCachedRequest */ + "use strict"; + + function get(address) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", address, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + resolve(); + } + }; + xhr.send(null); + }); + } + + async function performRequests() { + await get("sjs_status-codes-test-server.sjs?sts=100"); + await get("sjs_status-codes-test-server.sjs?sts=200"); + await get("sjs_status-codes-test-server.sjs?sts=300"); + await get("sjs_status-codes-test-server.sjs?sts=400"); + await get("sjs_status-codes-test-server.sjs?sts=500"); + } + + async function performCachedRequests() { + await get("sjs_status-codes-test-server.sjs?sts=ok&cached"); + await get("sjs_status-codes-test-server.sjs?sts=redirect&cached"); + } + + async function performOneCachedRequest() { + await get("sjs_status-codes-test-server.sjs?sts=ok&cached"); + await get("sjs_status-codes-test-server.sjs?sts=ok&cached"); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/html_tracking-protection.html b/devtools/client/netmonitor/test/html_tracking-protection.html new file mode 100644 index 0000000000..7329ebdc3e --- /dev/null +++ b/devtools/client/netmonitor/test/html_tracking-protection.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <script type="text/javascript"> + /* exported performRequests */ + "use strict"; + + function performRequests() { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "https://tracking.example.org/", true); + xhr.send(null); + } + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_websocket-test-page.html b/devtools/client/netmonitor/test/html_websocket-test-page.html new file mode 100644 index 0000000000..1b4202c269 --- /dev/null +++ b/devtools/client/netmonitor/test/html_websocket-test-page.html @@ -0,0 +1,10 @@ +<script> +"use strict"; +openSocket(); + +function openSocket() { + new WebSocket("ws://localhost:8080/"); +} + +new Worker("js_websocket-worker-test.js"); +</script> diff --git a/devtools/client/netmonitor/test/html_worker-test-page.html b/devtools/client/netmonitor/test/html_worker-test-page.html new file mode 100644 index 0000000000..46f64368a1 --- /dev/null +++ b/devtools/client/netmonitor/test/html_worker-test-page.html @@ -0,0 +1,13 @@ +<script> +/* eslint-disable no-unused-vars */ +"use strict"; +startWorker(); + +var w; +function startWorker() { + startWorkerInner(); +} +function startWorkerInner() { + w = new Worker("js_worker-test.js"); +} +</script> diff --git a/devtools/client/netmonitor/test/html_ws-early-connection-page.html b/devtools/client/netmonitor/test/html_ws-early-connection-page.html new file mode 100644 index 0000000000..9bc062f85f --- /dev/null +++ b/devtools/client/netmonitor/test/html_ws-early-connection-page.html @@ -0,0 +1,24 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype HTML> +<html> + <head> + <meta charset="utf-8"/> + <title>WebSocket Inspection Test Page</title> + </head> + <body> + <h1>WebSocket Inspection Test Page</h1> + <script type="text/javascript"> + "use strict"; + const ws = new WebSocket( + "ws://mochi.test:8888/browser/devtools/client/netmonitor/test/file_ws_backend"); + + ws.onopen = () => { + ws.send("readyState:" + document.readyState); + ws.close(); + } + </script> + <script type="text/javascript" + src="http://example.com/browser/devtools/client/netmonitor/test/sjs_slow-script-server.sjs"></script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/html_ws-sse-test-page.html b/devtools/client/netmonitor/test/html_ws-sse-test-page.html new file mode 100644 index 0000000000..8936efa81e --- /dev/null +++ b/devtools/client/netmonitor/test/html_ws-sse-test-page.html @@ -0,0 +1,59 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + <!doctype HTML> + <html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>WebSocket/SSE Inspection Test Page</title> + </head> + <body> + <h1>WebSocket/SSE Inspection Test Page</h1> + <script type="text/javascript"> + /* exported openWsConnection, openSseConnection, closeWsConnection, sendData */ + "use strict"; + + let ws; + function openWsConnection(numFramesToSend) { + return new Promise(resolve => { + ws = new WebSocket( + "ws://mochi.test:8888/browser/devtools/client/netmonitor/test/file_ws_backend" + ); + + ws.onopen = e => { + for (let i = 0; i < numFramesToSend; i++) { + ws.send("Payload " + i); + } + resolve(); + }; + }); + } + + function closeWsConnection() { + return new Promise(resolve => { + ws.onclose = e => { + resolve(); + }; + ws.close(); + }); + } + + function sendData(payload) { + ws.send(payload); + } + + let es; + function openSseConnection() { + return new Promise(resolve => { + es = new EventSource("sjs_sse-test-server.sjs"); + es.onmessage = function(e) { + es.close(); + resolve(); + }; + }); + } + </script> + </body> + </html> diff --git a/devtools/client/netmonitor/test/html_ws-test-page.html b/devtools/client/netmonitor/test/html_ws-test-page.html new file mode 100644 index 0000000000..de32b2a18a --- /dev/null +++ b/devtools/client/netmonitor/test/html_ws-test-page.html @@ -0,0 +1,58 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype HTML> +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>WebSocket Inspection Test Page</title> + </head> + <body> + <h1>WebSocket Inspection Test Page</h1> + <script type="text/javascript"> + /* exported openConnection, closeConnection, sendData, sendFrames */ + "use strict"; + + let ws; + function openConnection(numFramesToSend) { + return new Promise(resolve => { + ws = new WebSocket( + "ws://mochi.test:8888/browser/devtools/client/netmonitor/test/file_ws_backend"); + + ws.onopen = e => { + for (let i = 0; i < numFramesToSend; i++) { + ws.send("Payload " + i); + } + resolve(); + }; + }); + } + + function sendFrames(numFramesToSend) { + return new Promise(resolve => { + for (let i = 0; i < numFramesToSend; i++) { + ws.send("Payload " + i); + } + resolve(); + }) + } + + function closeConnection() { + return new Promise(resolve => { + ws.onclose = e => { + resolve(); + } + ws.close(); + }) + } + + function sendData(payload, asBinary = false) { + ws.send( + asBinary ? Uint8Array.from(payload, c => c.charCodeAt(0)) : payload + ); + } + </script> + </body> +</html> diff --git a/devtools/client/netmonitor/test/js_websocket-worker-test.js b/devtools/client/netmonitor/test/js_websocket-worker-test.js new file mode 100644 index 0000000000..9920f09cb8 --- /dev/null +++ b/devtools/client/netmonitor/test/js_websocket-worker-test.js @@ -0,0 +1,6 @@ +"use strict"; +openWorkerSocket(); + +function openWorkerSocket() { + new WebSocket("wss://localhost:8081"); +} diff --git a/devtools/client/netmonitor/test/js_worker-test.js b/devtools/client/netmonitor/test/js_worker-test.js new file mode 100644 index 0000000000..7b9538b0b9 --- /dev/null +++ b/devtools/client/netmonitor/test/js_worker-test.js @@ -0,0 +1,30 @@ +/* eslint-disable no-unused-vars, no-undef */ +"use strict"; +startWorkerFromWorker(); + +var w; +function startWorkerFromWorker() { + w = new Worker("js_worker-test2.js"); +} + +importScriptsFromWorker(); + +function importScriptsFromWorker() { + try { + importScripts("missing1.js", "missing2.js"); + } catch (e) {} +} + +createJSONRequest(); + +function createJSONRequest() { + const request = new XMLHttpRequest(); + request.open("GET", "missing.json", true); + request.send(null); +} + +fetchThing(); + +function fetchThing() { + fetch("missing.txt"); +} diff --git a/devtools/client/netmonitor/test/js_worker-test2.js b/devtools/client/netmonitor/test/js_worker-test2.js new file mode 100644 index 0000000000..9ee307f2ec --- /dev/null +++ b/devtools/client/netmonitor/test/js_worker-test2.js @@ -0,0 +1,3 @@ +"use strict"; + +console.log("I AM A WORKER"); diff --git a/devtools/client/netmonitor/test/node/jest.config.js b/devtools/client/netmonitor/test/node/jest.config.js new file mode 100644 index 0000000000..7491db85b9 --- /dev/null +++ b/devtools/client/netmonitor/test/node/jest.config.js @@ -0,0 +1,12 @@ +/* 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, +}; diff --git a/devtools/client/netmonitor/test/node/package.json b/devtools/client/netmonitor/test/node/package.json new file mode 100644 index 0000000000..ff1a6a0e7e --- /dev/null +++ b/devtools/client/netmonitor/test/node/package.json @@ -0,0 +1,15 @@ +{ + "name": "netmonitor-tests", + "license": "MPL-2.0", + "version": "0.0.1", + "engines": { + "node": ">=8.9.4" + }, + "scripts": { + "test": "jest", + "test-ci": "jest --json" + }, + "dependencies": { + "jest": "^24.6.0" + } +} diff --git a/devtools/client/netmonitor/test/node/reducers/sort.spec.js b/devtools/client/netmonitor/test/node/reducers/sort.spec.js new file mode 100644 index 0000000000..4afd7c0fe3 --- /dev/null +++ b/devtools/client/netmonitor/test/node/reducers/sort.spec.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + Sort, + sortReducer, +} = require("resource://devtools/client/netmonitor/src/reducers/sort.js"); +const { + SORT_BY, +} = require("resource://devtools/client/netmonitor/src/constants.js"); + +describe("sorting reducer", () => { + it("it should sort by sort type", () => { + const initialState = new Sort(); + const action = { + type: SORT_BY, + sortType: "TimeWhen", + }; + const expectedState = { + type: "TimeWhen", + ascending: true, + }; + + expect(expectedState).toEqual(sortReducer(initialState, action)); + }); +}); diff --git a/devtools/client/netmonitor/test/node/yarn.lock b/devtools/client/netmonitor/test/node/yarn.lock new file mode 100644 index 0000000000..8d3b6674c0 --- /dev/null +++ b/devtools/client/netmonitor/test/node/yarn.lock @@ -0,0 +1,3553 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" + integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/core@^7.1.0": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" + integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.5.5" + "@babel/helpers" "^7.5.5" + "@babel/parser" "^7.5.5" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.5.5" + "@babel/types" "^7.5.5" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.5.tgz#873a7f936a3c89491b43536d12245b626664e3cf" + integrity sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ== + dependencies: + "@babel/types" "^7.5.5" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@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" + integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== + 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" + integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== + 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" + integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== + +"@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" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + +"@babel/helpers@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.5.tgz#63908d2a73942229d1e6685bc2a0e730dde3b75e" + integrity sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g== + dependencies: + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.5.5" + "@babel/types" "^7.5.5" + +"@babel/highlight@^7.0.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" + integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== + 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.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" + integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== + +"@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" + integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== + 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" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + 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.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" + integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.5.5" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.5.5" + "@babel/types" "^7.5.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" + integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + 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" + integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA== + 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" + integrity sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg== + 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" + integrity sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A== + 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" + integrity sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw== + 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" + integrity sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw== + 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" + integrity sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw== + 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" + integrity sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag== + 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" + integrity sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng== + 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" + integrity sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg== + 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" + integrity sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA== + 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" + integrity sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg== + 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" + integrity sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg== + 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" + integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ== + 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" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + 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" + integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw== + 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" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@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" + integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + 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" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@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" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@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" + integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw== + +abab@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" + integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +acorn-globals@^4.1.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.2.tgz#4e2c2313a597fd589720395f6354b41cd5ec8006" + integrity sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ== + dependencies: + acorn "^6.0.1" + acorn-walk "^6.0.1" + +acorn-walk@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" + integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== + +acorn@^5.5.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + +acorn@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.0.tgz#67f0da2fc339d6cfb5d6fb244fd449f33cd8bbe3" + integrity sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw== + +ajv@^6.5.5: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + 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" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +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" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +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" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + 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" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + 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" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +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" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + 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" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + 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" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +babel-jest@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.8.0.tgz#5c15ff2b28e20b0f45df43fe6b7f2aae93dba589" + integrity sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw== + 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" + integrity sha512-dySz4VJMH+dpndj0wjJ8JPs/7i1TdSPb1nRrn56/92pKOF9VKC1FMFJmMXjzlGGusnCAqujP6PBCiKq0sVA+YQ== + 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" + integrity sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w== + 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" + integrity sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw== + 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" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + 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" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + 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" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + 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" + integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== + +browser-resolve@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== + dependencies: + resolve "1.1.7" + +bser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.0.tgz#65fc784bf7f87c009b973c12db6546902fa9c7b5" + integrity sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg== + 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" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + 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" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + 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" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +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" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chownr@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" + integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + 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" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + 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" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +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" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + 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" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + 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" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +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" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@~2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +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" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +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" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + 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" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +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" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" + integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== + 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" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + 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" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== + 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" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + 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" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + 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" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + 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" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +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" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + 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" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + 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" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + 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" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + 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" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +diff-sequences@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975" + integrity sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw== + +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +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" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.5.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== + 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" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + 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" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escodegen@^1.9.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" + integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== + 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" + integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= + +estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= + +exec-sh@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" + integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + 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" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + 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" + integrity sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA== + 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" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + 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" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + 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" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + 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: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +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" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +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" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= + dependencies: + bser "^2.0.0" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + 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" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + 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" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + 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" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + 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" + integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== + 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" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + dependencies: + nan "^2.12.1" + node-pre-gyp "^0.12.0" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + 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" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + 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" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + 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" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + 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" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" + integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +handlebars@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + 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" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + 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" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + 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" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + 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" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + 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" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + 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" + integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== + +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" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== + dependencies: + whatwg-encoding "^1.0.1" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + 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" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + 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" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + 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" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + 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" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +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" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + 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" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + 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" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + 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" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + 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" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + 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" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + 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" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + 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" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + 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" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + 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" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +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" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +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" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + 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" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= + dependencies: + has "^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" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + 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" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + 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" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +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" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +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" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + 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" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + 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" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + 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" + integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== + 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" + integrity sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug== + 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" + integrity sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA== + 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" + integrity sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw== + 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" + integrity sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g== + 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" + integrity sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg== + 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" + integrity sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA== + 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" + integrity sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ== + 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" + integrity sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q== + 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" + integrity sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ== + +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" + integrity sha512-SwaxMGVdAZk3ernAx2Uv2sorA7jm3Kx+lR0grp6rMmnY06Kn/urtKx1LPN2mGTea4fCT38impYT28FfcLUhX0g== + 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" + integrity sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong== + 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" + integrity sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g== + 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" + integrity sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw== + 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" + integrity sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g== + 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" + integrity sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A== + 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" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== + +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" + integrity sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg== + +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" + integrity sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw== + 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" + integrity sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw== + 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" + integrity sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow== + 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" + integrity sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA== + 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" + integrity sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q== + +jest-snapshot@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.8.0.tgz#3bec6a59da2ff7bc7d097a853fb67f9d415cb7c6" + integrity sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg== + 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" + integrity sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA== + 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" + integrity sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA== + 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" + integrity sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw== + 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" + integrity sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ== + dependencies: + merge-stream "^1.0.1" + supports-color "^6.1.0" + +jest@^24.6.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.8.0.tgz#d5dff1984d0d1002196e9b7f12f75af1b2809081" + integrity sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg== + 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" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^11.5.1: + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== + 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" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +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" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +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" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +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" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" + integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + 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" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + 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" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + 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" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + 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" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +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" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + +kleur@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + 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" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== + +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + 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" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + 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" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash@^4.17.11, lodash@^4.17.13: + version "4.17.14" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" + integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + 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" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + 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" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + 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" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + 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" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + 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" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + 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" + integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= + 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" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + 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" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +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" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + 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" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.1, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +minipass@^2.2.1, minipass@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" + integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== + 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" + integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== + dependencies: + minipass "^2.2.1" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + 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" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + 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" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== + 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" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +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" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + +node-notifier@^5.2.1: + version "5.4.0" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.0.tgz#7b455fdce9f7de0c63538297354f3db468426e6a" + integrity sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ== + 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" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== + 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" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + 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" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + 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" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + 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" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== + +npm-packlist@^1.1.6: + version "1.4.4" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" + integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== + 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" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + 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" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.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" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nwsapi@^2.0.7: + version "2.1.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" + integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + 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" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +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" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + 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" + integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= + 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" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + 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" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + 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" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +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" + integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= + 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" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +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" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + +p-limit@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== + 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" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + 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" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + 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" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +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" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +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" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + 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" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + 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" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + 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" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== + +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" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +pretty-format@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" + integrity sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw== + 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" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +prompts@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.1.0.tgz#bf90bc71f6065d255ea2bdc0fe6520485c1b45db" + integrity sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg== + dependencies: + kleur "^3.0.2" + sisteransi "^1.0.0" + +psl@^1.1.24, psl@^1.1.28: + version "1.2.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.2.0.tgz#df12b5b1b3a30f51c329eacbdef98f3a6e136dc6" + integrity sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + 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" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-is@^16.8.4: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" + integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== + +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" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + 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" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + 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" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + 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" + +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== + dependencies: + util.promisify "^1.0.0" + +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" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + 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" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +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" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + 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" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + 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" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + 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" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +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" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +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" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + 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" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +resolve@^1.10.0, resolve@^1.3.2: + version "1.11.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" + integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== + 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" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +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" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +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" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + 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" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + 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" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +semver@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" + integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== + +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" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + 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" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +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" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +sisteransi@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.2.tgz#ec57d64b6f25c4f26c0e2c7dd23f2d7f12f7e418" + integrity sha512-ZcYcZcT69nSLAR2oLN2JwNmLkJEKGooFMCdvOkFrToUt/WfcRWqhIg4P4KwY4dmLbuyXIx4o4YmPsvMRJYJd/w== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + 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" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + 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" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + 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" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + 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" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + 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" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +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" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +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" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + 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" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +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" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +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" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + 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" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + 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" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + 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" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= + 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" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + 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" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + 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" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + 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" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + 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" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + 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" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +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" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + 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" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +symbol-tree@^3.2.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tar@^4: + version "4.4.10" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" + integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== + 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" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + 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" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +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" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +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" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + 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" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + 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" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + 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" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + 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" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + 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" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + 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" + integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + 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" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +uglify-js@^3.1.4: + version "3.6.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" + integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + dependencies: + commander "~2.20.0" + source-map "~0.6.1" + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + 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" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + 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" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + 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" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +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" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + 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" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + 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" + integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= + 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" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + 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" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +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" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +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" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + 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" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + 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" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9, which@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + 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" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + 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" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + 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" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +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" + integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg== + 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" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + 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" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +"y18n@^3.2.1 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^3.0.0, yallist@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" + integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== + +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + 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" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + 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/netmonitor/test/ostrich-black.ttf b/devtools/client/netmonitor/test/ostrich-black.ttf Binary files differnew file mode 100644 index 0000000000..a0ef8fe1c9 --- /dev/null +++ b/devtools/client/netmonitor/test/ostrich-black.ttf diff --git a/devtools/client/netmonitor/test/ostrich-regular.ttf b/devtools/client/netmonitor/test/ostrich-regular.ttf Binary files differnew file mode 100644 index 0000000000..9682c07350 --- /dev/null +++ b/devtools/client/netmonitor/test/ostrich-regular.ttf diff --git a/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js b/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js new file mode 100644 index 0000000000..282c02cfbb --- /dev/null +++ b/devtools/client/netmonitor/test/service-workers/status-codes-service-worker.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +self.addEventListener("activate", event => { + // start controlling the already loaded page + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("fetch", event => { + const response = new Response("Service worker response", { + statusText: "OK", + }); + event.respondWith(response); +}); diff --git a/devtools/client/netmonitor/test/service-workers/status-codes.html b/devtools/client/netmonitor/test/service-workers/status-codes.html new file mode 100644 index 0000000000..05664d13a7 --- /dev/null +++ b/devtools/client/netmonitor/test/service-workers/status-codes.html @@ -0,0 +1,64 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Status codes test</p> + + <script type="text/javascript"> + /* exported registerServiceWorker, unregisterServiceWorker, performRequests */ + "use strict"; + + let swRegistration; + + function registerServiceWorker() { + const sw = navigator.serviceWorker; + return sw.register("status-codes-service-worker.js") + .then(registration => { + swRegistration = registration; + console.log("Registered, scope is:", registration.scope); + return sw.ready; + }).then(() => { + // wait until the page is controlled + return new Promise(resolve => { + if (sw.controller) { + resolve(); + } else { + sw.addEventListener("controllerchange", function() { + resolve(); + }, {once: true}); + } + }); + }).catch(err => { + console.error("Registration failed"); + }); + } + + function unregisterServiceWorker() { + return swRegistration.unregister(); + } + + function performRequests() { + return Promise.all( + [ + fetch("sjs_content-type-test-server.sjs?sts=304&fmt=html"), + fetch("sjs_content-type-test-server.sjs?sts=304&fmt=css"), + fetch("sjs_content-type-test-server.sjs?sts=304&fmt=js"), + fetch("test-image.png?v=" + Math.random()) + ] + ); + } + </script> + </body> + +</html> diff --git a/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs new file mode 100644 index 0000000000..d9ac186396 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs @@ -0,0 +1,419 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function gzipCompressString(string, obs) { + const scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( + Ci.nsIStreamLoader + ); + listener.init(obs); + const converter = scs.asyncConvertData( + "uncompressed", + "gzip", + listener, + null + ); + const stringStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + stringStream.data = string; + converter.onStartRequest(null); + converter.onDataAvailable(null, stringStream, 0, string.length); + converter.onStopRequest(null, null); +} + +function doubleGzipCompressString(string, observer) { + const observer2 = { + onStreamComplete(loader, context, status, length, result) { + const buffer = String.fromCharCode.apply(this, result); + gzipCompressString(buffer, observer); + }, + }; + gzipCompressString(string, observer2); +} + +function handleRequest(request, response) { + response.processAsync(); + + const params = request.queryString.split("&"); + const format = (params.filter(s => s.includes("fmt="))[0] || "").split( + "=" + )[1]; + const status = + (params.filter(s => s.includes("sts="))[0] || "").split("=")[1] || 200; + const cookies = + (params.filter(s => s.includes("cookies="))[0] || "").split("=")[1] || 0; + const cors = request.queryString.includes("cors=1"); + + let cachedCount = 0; + const cacheExpire = 60; // seconds + + function setCacheHeaders() { + if (status != 304) { + response.setHeader( + "Cache-Control", + "no-cache, no-store, must-revalidate" + ); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + return; + } + // Spice things up a little! + if (cachedCount % 2) { + response.setHeader("Cache-Control", "max-age=" + cacheExpire, false); + } else { + response.setHeader( + "Expires", + Date(Date.now() + cacheExpire * 1000), + false + ); + } + cachedCount++; + } + + function setCookieHeaders() { + if (cookies) { + response.setHeader( + "Set-Cookie", + "name1=value1; Domain=.foo.example.com", + true + ); + response.setHeader( + "Set-Cookie", + "name2=value2; Domain=.example.com", + true + ); + } + } + + function setAllowOriginHeaders() { + if (cors) { + response.setHeader("Access-Control-Allow-Origin", "*", false); + } + } + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + timer.initWithCallback( + // eslint-disable-next-line complexity + () => { + // to avoid garbage collection + timer = null; + switch (format) { + case "txt": { + response.setStatusLine(request.httpVersion, status, "DA DA DA"); + response.setHeader("Content-Type", "text/plain", false); + setCacheHeaders(); + + function convertToUtf8(str) { + return String.fromCharCode(...new TextEncoder().encode(str)); + } + + // This script must be evaluated as UTF-8 for this to write out the + // bytes of the string in UTF-8. If it's evaluated as Latin-1, the + // written bytes will be the result of UTF-8-encoding this string + // *twice*. + const data = "Братан, ты вообще качаешься?"; + const stringOfUtf8Bytes = convertToUtf8(data); + response.write(stringOfUtf8Bytes); + + response.finish(); + break; + } + case "xml": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/xml; charset=utf-8", false); + setCacheHeaders(); + response.write("<label value='greeting'>Hello XML!</label>"); + response.finish(); + break; + } + case "html": { + const content = ( + params.filter(s => s.includes("res="))[0] || "" + ).split("=")[1]; + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setAllowOriginHeaders(); + setCacheHeaders(); + setCookieHeaders(); + response.write(content || "<p>Hello HTML!</p>"); + response.finish(); + break; + } + case "xhtml": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/xhtml+xml; charset=utf-8", + false + ); + setAllowOriginHeaders(); + setCacheHeaders(); + setCookieHeaders(); + response.write("<p>Hello XHTML!</p>"); + response.finish(); + break; + } + case "html-long": { + const str = new Array(102400 /* 100 KB in bytes */).join("."); + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setCacheHeaders(); + response.write("<p>" + str + "</p>"); + response.finish(); + break; + } + case "css": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/css; charset=utf-8", false); + setCacheHeaders(); + response.write("body:pre { content: 'Hello CSS!' }"); + response.finish(); + break; + } + case "js": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/javascript; charset=utf-8", + false + ); + setCacheHeaders(); + response.write("function() { return 'Hello JS!'; }"); + response.finish(); + break; + } + case "json": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/json; charset=utf-8", + false + ); + setCacheHeaders(); + response.write('{ "greeting": "Hello JSON!" }'); + response.finish(); + break; + } + case "jsonp": { + const fun = (params.filter(s => s.includes("jsonp="))[0] || "").split( + "=" + )[1]; + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/json; charset=utf-8", false); + setCacheHeaders(); + response.write(fun + '({ "greeting": "Hello JSONP!" })'); + response.finish(); + break; + } + case "jsonp2": { + const fun = (params.filter(s => s.includes("jsonp="))[0] || "").split( + "=" + )[1]; + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/json; charset=utf-8", false); + setCacheHeaders(); + response.write( + " " + fun + ' ( { "greeting": "Hello weird JSONP!" } ) ; ' + ); + response.finish(); + break; + } + case "json-b64": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/json; charset=utf-8", false); + setCacheHeaders(); + response.write(btoa('{ "greeting": "This is a base 64 string." }')); + response.finish(); + break; + } + case "json-long": { + const str = '{ "greeting": "Hello long string JSON!" },'; + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/json; charset=utf-8", false); + setCacheHeaders(); + response.write("[" + new Array(2048).join(str).slice(0, -1) + "]"); + response.finish(); + break; + } + case "json-malformed": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/json; charset=utf-8", false); + setCacheHeaders(); + response.write('{ "greeting": "Hello malformed JSON!" },'); + response.finish(); + break; + } + case "json-text-mime": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "text/plain; charset=utf-8", + false + ); + setCacheHeaders(); + response.write('{ "greeting": "Hello third-party JSON!" }'); + response.finish(); + break; + } + case "json-custom-mime": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "text/x-bigcorp-json; charset=utf-8", + false + ); + setCacheHeaders(); + response.write('{ "greeting": "Hello oddly-named JSON!" }'); + response.finish(); + break; + } + case "json-valid-xssi-protection": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/json; charset=utf-8", false); + setCacheHeaders(); + response.write(')]}\'\n{"greeting": "Hello good XSSI protection"}'); + response.finish(); + break; + } + + case "font": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "font/woff", false); + setAllowOriginHeaders(); + setCacheHeaders(); + response.finish(); + break; + } + case "image": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "image/png", false); + setCacheHeaders(); + response.finish(); + break; + } + case "application-ogg": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "application/ogg", false); + setCacheHeaders(); + response.finish(); + break; + } + case "audio": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "audio/ogg", false); + setCacheHeaders(); + response.finish(); + break; + } + case "video": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "video/webm", false); + setCacheHeaders(); + response.finish(); + break; + } + case "flash": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/x-shockwave-flash", + false + ); + setCacheHeaders(); + response.finish(); + break; + } + case "ws": { + response.setStatusLine( + request.httpVersion, + 101, + "Switching Protocols" + ); + response.setHeader("Connection", "upgrade", false); + response.setHeader("Upgrade", "websocket", false); + setCacheHeaders(); + response.finish(); + break; + } + case "gzip": { + // Note: we're doing a double gzip encoding to test multiple + // converters in network monitor. + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Encoding", "gzip\t ,gzip", false); + setCacheHeaders(); + + const observer = { + onStreamComplete(loader, context, statusl, length, result) { + const buffer = String.fromCharCode.apply(this, result); + response.setHeader("Content-Length", "" + buffer.length, false); + response.write(buffer); + response.finish(); + }, + }; + const data = new Array(1000).join("Hello gzip!"); + doubleGzipCompressString(data, observer); + break; + } + case "br": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/json", false); + response.setHeader("Content-Encoding", "br", false); + setCacheHeaders(); + response.setHeader("Content-Length", "10", false); + // Use static data since we cannot encode brotli. + response.write("\x1b\x3f\x00\x00\x24\xb0\xe2\x99\x80\x12"); + response.finish(); + break; + } + case "hls-m3u8": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "application/x-mpegurl", false); + setCacheHeaders(); + response.write("#EXTM3U\n"); + response.finish(); + break; + } + case "hls-m3u8-alt-mime-type": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/vnd.apple.mpegurl", + false + ); + setCacheHeaders(); + response.write("#EXTM3U\n"); + response.finish(); + break; + } + case "mpeg-dash": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "video/vnd.mpeg.dash.mpd", false); + setCacheHeaders(); + response.write('<?xml version="1.0" encoding="UTF-8"?>\n'); + response.write( + '<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></MPD>\n' + ); + response.finish(); + break; + } + default: { + response.setStatusLine(request.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setCacheHeaders(); + response.write("<blink>Not Found</blink>"); + response.finish(); + break; + } + } + }, + 10, + Ci.nsITimer.TYPE_ONE_SHOT + ); // Make sure this request takes a few ms. +} diff --git a/devtools/client/netmonitor/test/sjs_cors-test-server.sjs b/devtools/client/netmonitor/test/sjs_cors-test-server.sjs new file mode 100644 index 0000000000..3c3481e5c1 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_cors-test-server.sjs @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "Och Aye"); + + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Headers", "content-type", false); + + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + response.write("Access-Control-Allow-Origin: *"); +} diff --git a/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs b/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs new file mode 100644 index 0000000000..8160508c51 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_hsts-test-server.sjs @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + + if (request.queryString === "reset") { + // Reset the HSTS policy, prevent influencing other tests + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Strict-Transport-Security", "max-age=0"); + response.write("Resetting HSTS"); + } else if (request.scheme === "http") { + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Location", "https://" + request.host + request.path); + } else { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Strict-Transport-Security", "max-age=100"); + response.write("Page was accessed over HTTPS!"); + } +} diff --git a/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs b/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs new file mode 100644 index 0000000000..f0a5c95ab8 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_https-redirect-test-server.sjs @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + + response.setHeader("Access-Control-Allow-Origin", "*", false); + + if (request.scheme === "http") { + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Location", "https://" + request.host + request.path); + } else { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write("Page was accessed over HTTPS!"); + } +} diff --git a/devtools/client/netmonitor/test/sjs_json-test-server.sjs b/devtools/client/netmonitor/test/sjs_json-test-server.sjs new file mode 100644 index 0000000000..617ef04ae1 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_json-test-server.sjs @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + + response.setHeader("Content-Type", "application/json; charset=utf-8", false); + + // This server checks the name parameter from the request to decide which JSON object to + // return. + const params = request.queryString.split("&"); + const name = (params.filter(s => s.includes("name="))[0] || "").split("=")[1]; + switch (name) { + case "null": + response.write('{ "greeting": null }'); + break; + case "nogrip": + response.write('{"obj": {"type": "string" }}'); + break; + case "empty": + response.write("{}"); + break; + } +} diff --git a/devtools/client/netmonitor/test/sjs_long-polling-server.sjs b/devtools/client/netmonitor/test/sjs_long-polling-server.sjs new file mode 100644 index 0000000000..2c80149223 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_long-polling-server.sjs @@ -0,0 +1,35 @@ +"use strict"; + +const key = "blocked-response"; +function setResponse(response) { + setObjectState(key, response); +} + +function getResponse() { + let response; + getObjectState(key, v => { + response = v; + }); + return response; +} + +function handleRequest(request, response) { + const { queryString } = request; + if (!queryString) { + // The default end point will return a blocked response. + // The response object will be stored and will be released + // when "?unblock" is called. + response.processAsync(); + response.setHeader("Content-Type", "text/plain", false); + response.write("Begin...\n"); + setResponse(response); + } else if (queryString == "unblock") { + // unblock the pending response + getResponse().finish(); + setResponse(null); + + // and return synchronously. + response.setHeader("Content-Type", "text/plain"); + response.write("ok"); + } +} diff --git a/devtools/client/netmonitor/test/sjs_method-test-server.sjs b/devtools/client/netmonitor/test/sjs_method-test-server.sjs new file mode 100644 index 0000000000..4a820f3bcc --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_method-test-server.sjs @@ -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 BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "Och Aye"); + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + let body = ""; + if (request.method == "POST") { + const bodyStream = new BinaryInputStream(request.bodyInputStream); + + let avail = 0; + while ((avail = bodyStream.available()) > 0) { + body += String.fromCharCode.apply( + String, + bodyStream.readByteArray(avail) + ); + } + } + + const contentType = request.hasHeader("content-type") + ? request.getHeader("content-type") + : ""; + + const bodyOutput = [request.method, contentType, body].join("\n"); + response.bodyOutputStream.write(bodyOutput, bodyOutput.length); +} diff --git a/devtools/client/netmonitor/test/sjs_search-test-server.sjs b/devtools/client/netmonitor/test/sjs_search-test-server.sjs new file mode 100644 index 0000000000..6b1ef7990f --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_search-test-server.sjs @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/xml; charset=utf-8", false); + response.setHeader("Set-Cookie", "name1=test;", true); + response.write("<label value='test'>Hello From XML!</label>"); +} diff --git a/devtools/client/netmonitor/test/sjs_set-cookie-same-site.sjs b/devtools/client/netmonitor/test/sjs_set-cookie-same-site.sjs new file mode 100644 index 0000000000..b21dcddbcd --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_set-cookie-same-site.sjs @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "Och Aye"); + + response.setHeader("Set-Cookie", "foo=bar; SameSite=Lax"); + + response.write("Hello world!"); +} diff --git a/devtools/client/netmonitor/test/sjs_simple-test-server.sjs b/devtools/client/netmonitor/test/sjs_simple-test-server.sjs new file mode 100644 index 0000000000..7396a87235 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_simple-test-server.sjs @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "Och Aye"); + + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + + response.setHeaderNoCheck("Set-Cookie", "bob=true; Max-Age=10; HttpOnly"); + response.setHeaderNoCheck("Set-Cookie", "tom=cool; Max-Age=10; HttpOnly"); + + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + response.setHeaderNoCheck("Foo-Bar", "baz"); + response.setHeaderNoCheck("Foo-Bar", "baz"); + response.setHeaderNoCheck("Foo-Bar", ""); + + response.write("Hello world!"); +} diff --git a/devtools/client/netmonitor/test/sjs_simple-unsorted-cookies-test-server.sjs b/devtools/client/netmonitor/test/sjs_simple-unsorted-cookies-test-server.sjs new file mode 100644 index 0000000000..04519f6955 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_simple-unsorted-cookies-test-server.sjs @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "Och Aye"); + + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + + response.setHeader("Set-Cookie", "tom=cool; Max-Age=10; HttpOnly", true); + response.setHeader("Set-Cookie", "bob=true; Max-Age=10; HttpOnly", true); + response.setHeader("Set-Cookie", "foo=bar; Max-Age=10; HttpOnly", true); + response.setHeader("Set-Cookie", "__proto__=2; Max-Age=10; HttpOnly", true); + + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + response.setHeader("Foo-Bar", "baz", false); + response.write("Hello world!"); +} diff --git a/devtools/client/netmonitor/test/sjs_slow-script-server.sjs b/devtools/client/netmonitor/test/sjs_slow-script-server.sjs new file mode 100644 index 0000000000..9e10310128 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_slow-script-server.sjs @@ -0,0 +1,18 @@ +"use strict"; + +let timer; + +const DELAY_MS = 2000; +function handleRequest(request, response) { + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + () => { + response.setHeader("Content-Type", "text/javascript", false); + response.write("console.log('script loaded')\n"); + response.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/devtools/client/netmonitor/test/sjs_slow-test-server.sjs b/devtools/client/netmonitor/test/sjs_slow-test-server.sjs new file mode 100644 index 0000000000..9e996384e6 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_slow-test-server.sjs @@ -0,0 +1,20 @@ +"use strict"; + +let timer; + +const DELAY_MS = 1000; +function handleRequest(request, response) { + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + () => { + response.setHeader("Content-Type", "text/html", false); + response.write( + "<body>Slow loading page for netmonitor test. You should never see this.</body>" + ); + response.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs b/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs new file mode 100644 index 0000000000..98cf232861 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_sorting-test-server.sjs @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.processAsync(); + + const params = request.queryString.split("&"); + const index = params.filter(s => s.includes("index="))[0].split("=")[1]; + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + // to avoid garbage collection + timer = null; + response.setStatusLine( + request.httpVersion, + index == 1 ? 101 : index * 100, + "Meh" + ); + + response.setHeader( + "Cache-Control", + "no-cache, no-store, must-revalidate" + ); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + + response.setHeader("Content-Type", "text/" + index, false); + response.write(new Array(index * 10).join(index)); // + 0.01 KB + response.finish(); + }, + 10, + Ci.nsITimer.TYPE_ONE_SHOT + ); // Make sure this request takes a few ms. +} diff --git a/devtools/client/netmonitor/test/sjs_sse-test-server.sjs b/devtools/client/netmonitor/test/sjs_sse-test-server.sjs new file mode 100644 index 0000000000..bbfc7679b5 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_sse-test-server.sjs @@ -0,0 +1,7 @@ +"use strict"; +function handleRequest(request, response) { + response.processAsync(); + response.setHeader("Content-Type", "text/event-stream"); + response.write("data: Why so serious?\n\n"); + response.finish(); +} diff --git a/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs b/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs new file mode 100644 index 0000000000..a8ea9b5f8f --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_status-codes-test-server.sjs @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + response.processAsync(); + + const params = request.queryString.split("&"); + const status = params.filter(s => s.includes("sts="))[0].split("=")[1]; + const cached = params.filter(s => s === "cached").length !== 0; + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + // to avoid garbage collection + timer = null; + switch (status) { + case "100": + response.setStatusLine( + request.httpVersion, + 101, + "Switching Protocols" + ); + break; + case "200": + response.setStatusLine(request.httpVersion, 202, "Created"); + break; + case "300": + response.setStatusLine(request.httpVersion, 303, "See Other"); + break; + case "304": + response.setStatusLine(request.httpVersion, 304, "Not Modified"); + break; + case "400": + response.setStatusLine(request.httpVersion, 404, "Not Found"); + break; + case "500": + response.setStatusLine(request.httpVersion, 501, "Not Implemented"); + break; + case "ok": + response.setStatusLine(request.httpVersion, 200, "OK"); + break; + case "redirect": + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "http://example.com/redirected"); + break; + } + + if (!cached) { + response.setHeader( + "Cache-Control", + "no-cache, no-store, must-revalidate" + ); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + } else { + response.setHeader( + "Cache-Control", + "no-transform,public,max-age=300,s-maxage=900" + ); + response.setHeader("Expires", "Thu, 01 Dec 2100 20:00:00 GMT"); + } + + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + response.write("Hello status code " + status + "!"); + response.finish(); + }, + 10, + Ci.nsITimer.TYPE_ONE_SHOT + ); // Make sure this request takes a few ms. +} diff --git a/devtools/client/netmonitor/test/sjs_timings-test-server.sjs b/devtools/client/netmonitor/test/sjs_timings-test-server.sjs new file mode 100644 index 0000000000..6446461039 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_timings-test-server.sjs @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const trailerServerTiming = [ + { metric: "metric3", duration: "99789.11", description: "time3" }, + { metric: "metric4", duration: "1112.13", description: "time4" }, +]; + +const responseServerTiming = [ + { metric: "metric1", duration: "123.4", description: "time1" }, + { metric: "metric2", duration: "0", description: "time2" }, +]; + +function handleRequest(request, response) { + const body = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write(createServerTimingHeader(responseServerTiming)); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.write(createServerTimingHeader(trailerServerTiming)); + response.write("\r\n"); + response.finish(); +} + +function createServerTimingHeader(headerData) { + let header = ""; + for (let i = 0; i < headerData.length; i++) { + header += + "Server-Timing: " + + headerData[i].metric + + ";" + + "dur=" + + headerData[i].duration + + ";" + + "desc=" + + headerData[i].description + + "\r\n"; + } + return header; +} diff --git a/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs b/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs new file mode 100644 index 0000000000..77ec88bf44 --- /dev/null +++ b/devtools/client/netmonitor/test/sjs_truncate-test-server.sjs @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +function handleRequest(request, response) { + const params = request.queryString.split("&"); + const limit = (params.filter(s => s.includes("limit="))[0] || "").split( + "=" + )[1]; + + response.setStatusLine(request.httpVersion, 200, "Och Aye"); + + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + + response.write("x".repeat(2 * parseInt(limit, 10))); + + response.write("Hello world!"); +} diff --git a/devtools/client/netmonitor/test/test-image.png b/devtools/client/netmonitor/test/test-image.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/devtools/client/netmonitor/test/test-image.png diff --git a/devtools/client/netmonitor/test/xhr_bundle.js b/devtools/client/netmonitor/test/xhr_bundle.js new file mode 100644 index 0000000000..00001de5a2 --- /dev/null +++ b/devtools/client/netmonitor/test/xhr_bundle.js @@ -0,0 +1,91 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +function reallydoxhr() { + let z = new XMLHttpRequest(); + z.open("get", "test-image.png", true); + z.send(); +} + +function doxhr() { + reallydoxhr(); +} + +window.doxhr = doxhr; + + +/***/ }) +/******/ ]); +//# sourceMappingURL=xhr_bundle.js.map
\ No newline at end of file diff --git a/devtools/client/netmonitor/test/xhr_bundle.js.map b/devtools/client/netmonitor/test/xhr_bundle.js.map new file mode 100644 index 0000000000..c28ee2a8f9 --- /dev/null +++ b/devtools/client/netmonitor/test/xhr_bundle.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap 1f90f505700f55e4a0b4","webpack:///./xhr_original.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;AChEA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA","file":"xhr_bundle.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 1f90f505700f55e4a0b4","\"use strict\";\n\nfunction reallydoxhr() {\n let z = new XMLHttpRequest();\n z.open(\"get\", \"test-image.png\", true);\n z.send();\n}\n\nfunction doxhr() {\n reallydoxhr();\n}\n\nwindow.doxhr = doxhr;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./xhr_original.js\n// module id = 0\n// module chunks = 0"],"sourceRoot":""}
\ No newline at end of file diff --git a/devtools/client/netmonitor/test/xhr_original.js b/devtools/client/netmonitor/test/xhr_original.js new file mode 100644 index 0000000000..8b480a0098 --- /dev/null +++ b/devtools/client/netmonitor/test/xhr_original.js @@ -0,0 +1,13 @@ +"use strict"; + +function reallydoxhr() { + const z = new XMLHttpRequest(); + z.open("get", "test-image.png", true); + z.send(); +} + +function doxhr() { + reallydoxhr(); +} + +window.doxhr = doxhr; diff --git a/devtools/client/netmonitor/test/xpcshell/.eslintrc.js b/devtools/client/netmonitor/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..8611c174f5 --- /dev/null +++ b/devtools/client/netmonitor/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/netmonitor/test/xpcshell/test_doc-utils.js b/devtools/client/netmonitor/test/xpcshell/test_doc-utils.js new file mode 100644 index 0000000000..7cd71662d6 --- /dev/null +++ b/devtools/client/netmonitor/test/xpcshell/test_doc-utils.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test for doc-utils + +"use strict"; + +function run_test() { + const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const MDN_URL = "https://developer.mozilla.org/docs/"; + const GTM_PARAMS_NM = + "?utm_source=mozilla" + + "&utm_medium=devtools-netmonitor&utm_campaign=default"; + const GTM_PARAMS_WC = + "?utm_source=mozilla" + + "&utm_medium=devtools-webconsole&utm_campaign=default"; + const USER_DOC_URL = "https://firefox-source-docs.mozilla.org/devtools-user/"; + + const { + getHeadersURL, + getHTTPStatusCodeURL, + getNetMonitorTimingsURL, + getPerformanceAnalysisURL, + getFilterBoxURL, + } = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js"); + + info("Checking for supported headers"); + equal( + getHeadersURL("Accept"), + `${MDN_URL}Web/HTTP/Headers/Accept${GTM_PARAMS_NM}` + ); + info("Checking for unsupported headers"); + equal(getHeadersURL("Width"), null); + + info("Checking for supported status code"); + equal( + getHTTPStatusCodeURL("200", "webconsole"), + `${MDN_URL}Web/HTTP/Status/200${GTM_PARAMS_WC}` + ); + info("Checking for unsupported status code"); + equal( + getHTTPStatusCodeURL("999", "webconsole"), + `${MDN_URL}Web/HTTP/Status${GTM_PARAMS_WC}` + ); + + equal( + getNetMonitorTimingsURL(), + `${USER_DOC_URL}network_monitor/request_details/#network-monitor-request-details-timings-tab` + ); + + equal( + getPerformanceAnalysisURL(), + `${USER_DOC_URL}network_monitor/performance_analysis/` + ); + + equal( + getFilterBoxURL(), + `${USER_DOC_URL}network_monitor/request_list/#filtering-by-properties` + ); +} diff --git a/devtools/client/netmonitor/test/xpcshell/test_request-utils-fetchNetworkUpdatePacket.js b/devtools/client/netmonitor/test/xpcshell/test_request-utils-fetchNetworkUpdatePacket.js new file mode 100644 index 0000000000..6b7a66f5ad --- /dev/null +++ b/devtools/client/netmonitor/test/xpcshell/test_request-utils-fetchNetworkUpdatePacket.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test devtools/client/netmonitor/src/utils/request-utils.js function +// |fetchNetworkUpdatePacket| + +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + fetchNetworkUpdatePacket, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +function run_test() { + let fetchedNetworkUpdateTypes = []; + async function mockConnectorRequestData(id, updateType) { + fetchedNetworkUpdateTypes.push(updateType); + return updateType; + } + + const updateTypes = ["requestHeaders", "responseHeaders"]; + const request = { + id: "foo", + requestHeadersAvailable: true, + responseHeadersAvailable: true, + }; + + info( + "Testing that the expected network update packets were fetched from the server" + ); + fetchNetworkUpdatePacket(mockConnectorRequestData, request, updateTypes); + equal(fetchedNetworkUpdateTypes.length, 2, "Two network request updates"); + equal( + fetchedNetworkUpdateTypes[0], + "requestHeaders", + "Request headers updates" + ); + equal( + fetchedNetworkUpdateTypes[1], + "responseHeaders", + " Response headers updates" + ); + + // clear the fetch updates for next test + fetchedNetworkUpdateTypes = []; + + info( + "Testing that no network updates were fetched when no `request` is provided" + ); + fetchNetworkUpdatePacket(mockConnectorRequestData, undefined, updateTypes); + equal(fetchedNetworkUpdateTypes.length, 0, "No network request updates"); +} diff --git a/devtools/client/netmonitor/test/xpcshell/test_request-utils-js-getFormattedProtocol.js b/devtools/client/netmonitor/test/xpcshell/test_request-utils-js-getFormattedProtocol.js new file mode 100644 index 0000000000..07ead52893 --- /dev/null +++ b/devtools/client/netmonitor/test/xpcshell/test_request-utils-js-getFormattedProtocol.js @@ -0,0 +1,235 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test devtools/client/netmonitor/src/utils/request-utils.js function +// |getFormattedProtocol| + +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + getFormattedProtocol, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +function run_test() { + const http_1p1_value_http1p1 = { + httpVersion: "HTTP/1.1", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "http/1.1", + }, + ], + }, + }; + + const http_1p1_value_http_no_slash_1p1 = { + httpVersion: "HTTP/1.1", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "http1.1", + }, + ], + }, + }; + + const http_1p1_value_http1p11 = { + httpVersion: "HTTP/1.1", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "http/1.11", + }, + ], + }, + }; + + const http_2p0_value_h2 = { + httpVersion: "HTTP/2.0", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "h2", + }, + ], + }, + }; + + const http_1p1_value_h1 = { + httpVersion: "HTTP/1.1", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "h1", + }, + ], + }, + }; + + const http_1p1_value_h2 = { + httpVersion: "HTTP/1.1", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "h2", + }, + ], + }, + }; + + const http_1p1_value_empty_string = { + httpVersion: "HTTP/1.1", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "", + }, + ], + }, + }; + + const http_2p0_value_empty_string = { + httpVersion: "HTTP/2.0", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "", + }, + ], + }, + }; + + const http_2p0_value_2p0 = { + httpVersion: "HTTP/2.0", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "HTTP/2.0", + }, + ], + }, + }; + + const http_3p0_value_h3 = { + httpVersion: "HTTP/3.0", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "h3", + }, + ], + }, + }; + + const http_3p0_value_h3p0 = { + httpVersion: "HTTP/3.0", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "h3.0", + }, + ], + }, + }; + + const http_3p0_value_http_3p0 = { + httpVersion: "HTTP/3.0", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "http/3.0", + }, + ], + }, + }; + + const http_3p0_value_3p0 = { + httpVersion: "HTTP/3.0", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "3.0", + }, + ], + }, + }; + + const http_4p0_value_h4 = { + httpVersion: "HTTP/4.0", + responseHeaders: { + headers: [ + { + name: "X-Firefox-Spdy", + value: "h4", + }, + ], + }, + }; + + info("Testing httpValue:HTTP/1.1, value:http/1.1"); + equal(getFormattedProtocol(http_1p1_value_http1p1), "HTTP/1.1"); + + info("Testing httpValue:HTTP/1.1, value:http1.1"); + equal( + getFormattedProtocol(http_1p1_value_http_no_slash_1p1), + "HTTP/1.1+http1.1" + ); + + info("Testing httpValue:HTTP/1.1, value:http/1.11"); + equal(getFormattedProtocol(http_1p1_value_http1p11), "HTTP/1.1+http/1.11"); + + info("Testing httpValue:HTTP/2.0, value:h2"); + equal(getFormattedProtocol(http_2p0_value_h2), "HTTP/2.0"); + + info("Testing httpValue:HTTP/1.1, value:h1"); + equal(getFormattedProtocol(http_1p1_value_h1), "HTTP/1.1+h1"); + + info("Testing httpValue:HTTP/1.1, value:h2"); + equal(getFormattedProtocol(http_1p1_value_h2), "HTTP/1.1+h2"); + + info("Testing httpValue:HTTP/1.1, value:http1.1"); + equal( + getFormattedProtocol(http_1p1_value_http_no_slash_1p1), + "HTTP/1.1+http1.1" + ); + + info("Testing httpValue:HTTP/1.1, value:''"); + equal(getFormattedProtocol(http_1p1_value_empty_string), "HTTP/1.1"); + + info("Testing httpValue:HTTP/2.0, value:''"); + equal(getFormattedProtocol(http_2p0_value_empty_string), "HTTP/2.0"); + + info("Testing httpValue:HTTP/2.0, value:HTTP/2.0"); + equal(getFormattedProtocol(http_2p0_value_2p0), "HTTP/2.0+HTTP/2.0"); + + info("Testing httpValue:HTTP/3.0, value:h3"); + equal(getFormattedProtocol(http_3p0_value_h3), "HTTP/3.0"); + + info("Testing httpValue:HTTP/3.0, value:h3.0"); + equal(getFormattedProtocol(http_3p0_value_h3p0), "HTTP/3.0"); + + info("Testing httpValue:HTTP/3.0, value:http/3.0"); + equal(getFormattedProtocol(http_3p0_value_http_3p0), "HTTP/3.0+http/3.0"); + + info("Testing httpValue:HTTP/3.0, value:3.0"); + equal(getFormattedProtocol(http_3p0_value_3p0), "HTTP/3.0+3.0"); + + info("Testing httpValue:HTTP/4.0, value:h4"); + equal(getFormattedProtocol(http_4p0_value_h4), "HTTP/4.0"); +} diff --git a/devtools/client/netmonitor/test/xpcshell/test_request-utils-parseJSON.js b/devtools/client/netmonitor/test/xpcshell/test_request-utils-parseJSON.js new file mode 100644 index 0000000000..78fd722522 --- /dev/null +++ b/devtools/client/netmonitor/test/xpcshell/test_request-utils-parseJSON.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test devtools/client/netmonitor/src/utils/request-utils.js function +// |parseJSON| ensure that it correctly handles plain JSON, Base 64 +// encoded JSON, and JSON that has XSSI protection prepended to it + +"use strict"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + parseJSON, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +function run_test() { + const validJSON = '{"item":{"subitem":true},"seconditem":"bar"}'; + const base64JSON = btoa(validJSON); + const parsedJSON = { item: { subitem: true }, seconditem: "bar" }; + const googleStyleXSSI = ")]}'\n"; + const facebookStyleXSSI = "for (;;);"; + const notRealXSSIPrevention = "sdgijsdjg"; + const while1XSSIPrevention = "while(1)"; + + const parsedValidJSON = parseJSON(validJSON); + info(JSON.stringify(parsedValidJSON)); + ok( + parsedValidJSON.json.item.subitem == parsedJSON.item.subitem && + parsedValidJSON.json.seconditem == parsedJSON.seconditem, + "plain JSON is parsed correctly" + ); + + const parsedBase64JSON = parseJSON(base64JSON); + ok( + parsedBase64JSON.json.item.subitem === parsedJSON.item.subitem && + parsedBase64JSON.json.seconditem === parsedJSON.seconditem, + "base64 encoded JSON is parsed correctly" + ); + + const parsedGoogleStyleXSSIJSON = parseJSON(googleStyleXSSI + validJSON); + ok( + parsedGoogleStyleXSSIJSON.strippedChars === googleStyleXSSI && + parsedGoogleStyleXSSIJSON.error === void 0 && + parsedGoogleStyleXSSIJSON.json.item.subitem === parsedJSON.item.subitem && + parsedGoogleStyleXSSIJSON.json.seconditem === parsedJSON.seconditem, + "Google style XSSI sequence correctly removed and returned" + ); + + const parsedFacebookStyleXSSIJSON = parseJSON(facebookStyleXSSI + validJSON); + ok( + parsedFacebookStyleXSSIJSON.strippedChars === facebookStyleXSSI && + parsedFacebookStyleXSSIJSON.error === void 0 && + parsedFacebookStyleXSSIJSON.json.item.subitem === + parsedJSON.item.subitem && + parsedFacebookStyleXSSIJSON.json.seconditem === parsedJSON.seconditem, + "Facebook style XSSI sequence correctly removed and returned" + ); + + const parsedWhileXSSIJSON = parseJSON(while1XSSIPrevention + validJSON); + ok( + parsedWhileXSSIJSON.strippedChars === while1XSSIPrevention && + parsedWhileXSSIJSON.error === void 0 && + parsedWhileXSSIJSON.json.item.subitem === parsedJSON.item.subitem && + parsedWhileXSSIJSON.json.seconditem === parsedJSON.seconditem, + "While XSSI sequence correctly removed and returned" + ); + const parsedInvalidJson = parseJSON(notRealXSSIPrevention + validJSON); + ok( + !parsedInvalidJson.json && !parsedInvalidJson.strippedChars, + "Parsed invalid JSON does not return a data object or strippedChars" + ); + equal( + parsedInvalidJson.error.name, + "SyntaxError", + "Parsing invalid JSON yeilds a SyntaxError" + ); +} diff --git a/devtools/client/netmonitor/test/xpcshell/xpcshell.toml b/devtools/client/netmonitor/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..399f22cbe2 --- /dev/null +++ b/devtools/client/netmonitor/test/xpcshell/xpcshell.toml @@ -0,0 +1,13 @@ +[DEFAULT] +tags = "devtools" +head = "" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] + +["test_doc-utils.js"] + +["test_request-utils-fetchNetworkUpdatePacket.js"] + +["test_request-utils-js-getFormattedProtocol.js"] + +["test_request-utils-parseJSON.js"] |