diff options
Diffstat (limited to 'devtools/client/jsonview/test')
45 files changed, 2249 insertions, 0 deletions
diff --git a/devtools/client/jsonview/test/array_json.json b/devtools/client/jsonview/test/array_json.json new file mode 100644 index 0000000000..a53a19fa70 --- /dev/null +++ b/devtools/client/jsonview/test/array_json.json @@ -0,0 +1 @@ +[{ "name": "jan" }, { "name": "honza" }, { "name": "odvarko" }] diff --git a/devtools/client/jsonview/test/array_json.json^headers^ b/devtools/client/jsonview/test/array_json.json^headers^ new file mode 100644 index 0000000000..6010bfd188 --- /dev/null +++ b/devtools/client/jsonview/test/array_json.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json; charset=utf-8 diff --git a/devtools/client/jsonview/test/browser.ini b/devtools/client/jsonview/test/browser.ini new file mode 100644 index 0000000000..0acb44c6b3 --- /dev/null +++ b/devtools/client/jsonview/test/browser.ini @@ -0,0 +1,59 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + array_json.json + array_json.json^headers^ + csp_json.json + csp_json.json^headers^ + empty.html + head.js + invalid_json.json + invalid_json.json^headers^ + manifest_json.json + manifest_json.json^headers^ + passthrough-sw.js + simple_json.json + simple_json.json^headers^ + valid_json.json + valid_json.json^headers^ + !/devtools/client/framework/test/head.js + !/devtools/client/shared/test/shared-head.js + !/devtools/client/shared/test/telemetry-test-helpers.js + +[browser_json_refresh.js] +[browser_jsonview_bug_1380828.js] +[browser_jsonview_chunked_json.js] +skip-if = http3 # Bug 1829298 +support-files = + chunked_json.sjs +[browser_jsonview_content_type.js] +[browser_jsonview_copy_headers.js] +[browser_jsonview_copy_json.js] +[browser_jsonview_copy_rawdata.js] +[browser_jsonview_csp_json.js] +[browser_jsonview_data_blocking.js] +[browser_jsonview_empty_object.js] +[browser_jsonview_encoding.js] +[browser_jsonview_filter.js] +[browser_jsonview_filter_clear.js] +[browser_jsonview_ignore_charset.js] +[browser_jsonview_initial_focus.js] +[browser_jsonview_invalid_json.js] +[browser_jsonview_manifest.js] +[browser_jsonview_nojs.js] +[browser_jsonview_nul.js] +[browser_jsonview_object-type.js] +[browser_jsonview_row_selection.js] +[browser_jsonview_save_json.js] +skip-if = http3 # Bug 1829298 +support-files = + !/toolkit/content/tests/browser/common/mockTransfer.js +[browser_jsonview_serviceworker.js] +[browser_jsonview_slash.js] +[browser_jsonview_theme.js] +[browser_jsonview_url_linkification.js] +[browser_jsonview_valid_json.js] +[browser_jsonview_expand_collapse.js] +skip-if = + os == 'linux' && bits == 64 && !debug # Bug 1794904 diff --git a/devtools/client/jsonview/test/browser_json_refresh.js b/devtools/client/jsonview/test/browser_json_refresh.js new file mode 100644 index 0000000000..0fbebbae57 --- /dev/null +++ b/devtools/client/jsonview/test/browser_json_refresh.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_FILE = "simple_json.json"; + +add_task(async function () { + info("Test JSON refresh started"); + + // generate file:// URI for JSON file and load in new tab + const dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append(TEST_JSON_FILE); + dir.normalize(); + const uri = Services.io.newFileURI(dir); + const tab = await addJsonViewTab(uri.spec); + + // perform sanity checks for URI and principals in loadInfo + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ TEST_JSON_FILE }], + // eslint-disable-next-line no-shadow + async function ({ TEST_JSON_FILE }) { + const channel = content.docShell.currentDocumentChannel; + const channelURI = channel.URI.spec; + ok( + channelURI.startsWith("file://") && channelURI.includes(TEST_JSON_FILE), + "sanity: correct channel uri" + ); + const contentPolicyType = channel.loadInfo.externalContentPolicyType; + is( + contentPolicyType, + Ci.nsIContentPolicy.TYPE_DOCUMENT, + "sanity: correct contentPolicyType" + ); + + const loadingPrincipal = channel.loadInfo.loadingPrincipal; + is(loadingPrincipal, null, "sanity: correct loadingPrincipal"); + const triggeringPrincipal = channel.loadInfo.triggeringPrincipal; + ok( + triggeringPrincipal.isSystemPrincipal, + "sanity: correct triggeringPrincipal" + ); + const principalToInherit = channel.loadInfo.principalToInherit; + ok( + principalToInherit.isNullPrincipal, + "sanity: correct principalToInherit" + ); + ok( + content.document.nodePrincipal.isNullPrincipal, + "sanity: correct doc.nodePrincipal" + ); + } + ); + + // reload the tab + await reloadBrowser(); + + // check principals in loadInfo are still correct + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ TEST_JSON_FILE }], + // eslint-disable-next-line no-shadow + async function ({ TEST_JSON_FILE }) { + // eslint-disable-line + const channel = content.docShell.currentDocumentChannel; + const channelURI = channel.URI.spec; + ok( + channelURI.startsWith("file://") && channelURI.includes(TEST_JSON_FILE), + "reloaded: correct channel uri" + ); + const contentPolicyType = channel.loadInfo.externalContentPolicyType; + is( + contentPolicyType, + Ci.nsIContentPolicy.TYPE_DOCUMENT, + "reloaded: correct contentPolicyType" + ); + + const loadingPrincipal = channel.loadInfo.loadingPrincipal; + is(loadingPrincipal, null, "reloaded: correct loadingPrincipal"); + const triggeringPrincipal = channel.loadInfo.triggeringPrincipal; + ok( + triggeringPrincipal.isSystemPrincipal, + "reloaded: correct triggeringPrincipal" + ); + const principalToInherit = channel.loadInfo.principalToInherit; + ok( + principalToInherit.isNullPrincipal, + "reloaded: correct principalToInherit" + ); + ok( + content.document.nodePrincipal.isNullPrincipal, + "reloaded: correct doc.nodePrincipal" + ); + } + ); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_bug_1380828.js b/devtools/client/jsonview/test/browser_jsonview_bug_1380828.js new file mode 100644 index 0000000000..6d698e8930 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_bug_1380828.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const VALID_JSON_URL = URL_ROOT + "valid_json.json"; +const INVALID_JSON_URL = URL_ROOT + "invalid_json.json"; +const prettyPrintButtonClass = ".textPanelBox .toolbar button.prettyprint"; + +add_task(async function () { + info("Test 'Pretty Print' button disappears on parsing invalid JSON"); + + const count = await testPrettyPrintButton(INVALID_JSON_URL); + is(count, 0, "There must be no pretty-print button for invalid json"); +}); + +add_task(async function () { + info("Test 'Pretty Print' button is present on parsing valid JSON"); + + const count = await testPrettyPrintButton(VALID_JSON_URL); + is(count, 1, "There must be pretty-print button for valid json"); +}); + +async function testPrettyPrintButton(url) { + await addJsonViewTab(url); + + await selectJsonViewContentTab("rawdata"); + info("Switched to Raw Data tab."); + + const count = await getElementCount(prettyPrintButtonClass); + return count; +} diff --git a/devtools/client/jsonview/test/browser_jsonview_chunked_json.js b/devtools/client/jsonview/test/browser_jsonview_chunked_json.js new file mode 100644 index 0000000000..56fe611486 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_chunked_json.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT_SSL + "chunked_json.sjs"; + +add_task(async function () { + info("Test chunked JSON started"); + + await addJsonViewTab(TEST_JSON_URL, { + appReadyState: "interactive", + docReadyState: "loading", + }); + + is( + await getElementCount(".rawdata.is-active"), + 1, + "The Raw Data tab is selected." + ); + + // Write some text and check that it is displayed. + await write("["); + await checkText(); + + // Repeat just in case. + await write("1,"); + await checkText(); + + is( + await getElementCount("button.prettyprint"), + 0, + "There is no pretty print button during load" + ); + + await selectJsonViewContentTab("json"); + is( + await getElementText(".jsonPanelBox > .panelContent"), + "", + "There is no JSON tree" + ); + + await selectJsonViewContentTab("headers"); + ok( + await getElementText(".headersPanelBox .netInfoHeadersTable"), + "The headers table has been filled." + ); + + // Write some text without being in Raw Data, then switch tab and check. + await write("2"); + await selectJsonViewContentTab("rawdata"); + await checkText(); + + // Add an array, when counting rows we will ensure it has been expanded automatically. + await write(",[3]]"); + await checkText(); + + // Close the connection. + + // When the ready state of the JSON View app changes, it triggers the + // custom event "AppReadyStateChange". + const appReady = BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "AppReadyStateChange", + true, + null, + true + ); + await server("close"); + await appReady; + + is(await getElementCount(".json.is-active"), 1, "The JSON tab is selected."); + + is( + await getElementCount(".jsonPanelBox .treeTable .treeRow"), + 4, + "There is a tree with 4 rows." + ); + + await selectJsonViewContentTab("rawdata"); + await checkText(); + + is( + await getElementCount("button.prettyprint"), + 1, + "There is a pretty print button." + ); + await clickJsonNode("button.prettyprint"); + await checkText(JSON.stringify(JSON.parse(data), null, 2)); +}); + +let data = " "; +async function write(text) { + data += text; + const onJsonViewUpdated = SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return new Promise(resolve => { + const observer = new content.MutationObserver(() => resolve()); + observer.observe(content.wrappedJSObject.JSONView.json, { + characterData: true, + }); + }); + } + ); + await server("write", text); + await onJsonViewUpdated; +} +async function checkText(text = data) { + is(await getElementText(".textPanelBox .data"), text, "Got the right text."); +} + +function server(action, value) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", TEST_JSON_URL + "?" + action + "=" + value); + xhr.addEventListener("load", resolve, { once: true }); + xhr.send(); + }); +} diff --git a/devtools/client/jsonview/test/browser_jsonview_content_type.js b/devtools/client/jsonview/test/browser_jsonview_content_type.js new file mode 100644 index 0000000000..c3298cc7d9 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_content_type.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); +const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +const contentTypes = { + valid: [ + "application/json", + "application/manifest+json", + "application/vnd.api+json", + "application/hal+json", + "application/json+json", + "application/whatever+json", + ], + invalid: [ + "text/json", + "text/hal+json", + "application/jsona", + "application/whatever+jsona", + ], +}; + +add_task(async function () { + info("Test JSON content types started"); + + // Prevent saving files to disk. + const useDownloadDir = SpecialPowers.getBoolPref( + "browser.download.useDownloadDir" + ); + SpecialPowers.setBoolPref("browser.download.useDownloadDir", false); + const { MockFilePicker } = SpecialPowers; + MockFilePicker.init(window); + MockFilePicker.returnValue = MockFilePicker.returnCancel; + + for (const kind of Object.keys(contentTypes)) { + const isValid = kind === "valid"; + for (const type of contentTypes[kind]) { + // Prevent "Open or Save" dialogs, which would make the test fail. + const mimeInfo = mimeSvc.getFromTypeAndExtension(type, null); + const exists = handlerSvc.exists(mimeInfo); + const { alwaysAskBeforeHandling } = mimeInfo; + mimeInfo.alwaysAskBeforeHandling = false; + handlerSvc.store(mimeInfo); + + await testType(isValid, type); + await testType(isValid, type, ";foo=bar+json"); + + // Restore old nsIMIMEInfo + if (exists) { + Object.assign(mimeInfo, { alwaysAskBeforeHandling }); + handlerSvc.store(mimeInfo); + } else { + handlerSvc.remove(mimeInfo); + } + } + } + + // Restore old pref + registerCleanupFunction(function () { + MockFilePicker.cleanup(); + SpecialPowers.setBoolPref( + "browser.download.useDownloadDir", + useDownloadDir + ); + }); +}); + +function testType(isValid, type, params = "") { + const TEST_JSON_URL = "data:" + type + params + ",[1,2,3]"; + return addJsonViewTab(TEST_JSON_URL).then( + async function () { + ok(isValid, "The JSON Viewer should only load for valid content types."); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [type], + function (contentType) { + is( + content.document.contentType, + contentType, + "Got the right content type" + ); + } + ); + + const count = await getElementCount(".jsonPanelBox .treeTable .treeRow"); + is(count, 3, "There must be expected number of rows"); + }, + function () { + ok( + !isValid, + "The JSON Viewer should only not load for invalid content types." + ); + } + ); +} diff --git a/devtools/client/jsonview/test/browser_jsonview_copy_headers.js b/devtools/client/jsonview/test/browser_jsonview_copy_headers.js new file mode 100644 index 0000000000..faa6e43354 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_copy_headers.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "valid_json.json"; + +add_task(async function () { + info("Test valid JSON started"); + + await addJsonViewTab(TEST_JSON_URL); + + // Select the RawData tab + await selectJsonViewContentTab("headers"); + + // Check displayed headers + const count = await getElementCount(".headersPanelBox .netHeadersGroup"); + is(count, 2, "There must be two header groups"); + + const text = await getElementText(".headersPanelBox .netInfoHeadersTable"); + isnot(text, "", "Headers text must not be empty"); + + const browser = gBrowser.selectedBrowser; + + // Verify JSON copy into the clipboard. + await waitForClipboardPromise( + function setup() { + BrowserTestUtils.synthesizeMouseAtCenter( + ".headersPanelBox .toolbar button.copy", + {}, + browser + ); + }, + function validator(value) { + return value.indexOf("application/json") > 0; + } + ); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_copy_json.js b/devtools/client/jsonview/test/browser_jsonview_copy_json.js new file mode 100644 index 0000000000..56c7b3ff75 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_copy_json.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "simple_json.json"; + +add_task(async function () { + info("Test copy JSON started"); + + await addJsonViewTab(TEST_JSON_URL); + + const countBefore = await getElementCount( + ".jsonPanelBox .treeTable .treeRow" + ); + ok(countBefore == 1, "There must be one row"); + + const text = await getElementText(".jsonPanelBox .treeTable .treeRow"); + is(text, 'name"value"', "There must be proper JSON displayed"); + + // Verify JSON copy into the clipboard. + const value = '{ "name": "value" }\n'; + const browser = gBrowser.selectedBrowser; + const selector = ".jsonPanelBox .toolbar button.copy"; + await waitForClipboardPromise( + function setup() { + BrowserTestUtils.synthesizeMouseAtCenter(selector, {}, browser); + }, + function validator(result) { + const str = normalizeNewLines(result); + return str == value; + } + ); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_copy_rawdata.js b/devtools/client/jsonview/test/browser_jsonview_copy_rawdata.js new file mode 100644 index 0000000000..0685b7c5d3 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_copy_rawdata.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "simple_json.json"; + +const jsonText = '{ "name": "value" }\n'; +const prettyJson = '{\n "name": "value"\n}'; + +add_task(async function () { + info("Test copy raw data started"); + + await addJsonViewTab(TEST_JSON_URL); + + // Select the RawData tab + await selectJsonViewContentTab("rawdata"); + + // Check displayed JSON + const text = await getElementText(".textPanelBox .data"); + is(text, jsonText, "Proper JSON must be displayed in DOM"); + + const browser = gBrowser.selectedBrowser; + + // Verify JSON copy into the clipboard. + await waitForClipboardPromise(function setup() { + BrowserTestUtils.synthesizeMouseAtCenter( + ".textPanelBox .toolbar button.copy", + {}, + browser + ); + }, jsonText); + + // Click 'Pretty Print' button + await BrowserTestUtils.synthesizeMouseAtCenter( + ".textPanelBox .toolbar button.prettyprint", + {}, + browser + ); + + let prettyText = await getElementText(".textPanelBox .data"); + prettyText = normalizeNewLines(prettyText); + ok( + prettyText.startsWith(prettyJson), + "Pretty printed JSON must be displayed" + ); + + // Verify JSON copy into the clipboard. + await waitForClipboardPromise( + function setup() { + BrowserTestUtils.synthesizeMouseAtCenter( + ".textPanelBox .toolbar button.copy", + {}, + browser + ); + }, + function validator(value) { + const str = normalizeNewLines(value); + return str == prettyJson; + } + ); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_csp_json.js b/devtools/client/jsonview/test/browser_jsonview_csp_json.js new file mode 100644 index 0000000000..4416bccf15 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_csp_json.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "csp_json.json"; + +add_task(async function () { + info("Test CSP JSON started"); + + const tab = await addJsonViewTab(TEST_JSON_URL); + + const count = await getElementCount(".jsonPanelBox .treeTable .treeRow"); + is(count, 1, "There must be one row"); + + // The JSON Viewer alters the CSP, but the displayed header should be the original one + await selectJsonViewContentTab("headers"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const responseHeaders = content.document.querySelector(".netHeadersGroup"); + const names = responseHeaders.querySelectorAll(".netInfoParamName"); + let found = false; + for (const name of names) { + if (name.textContent.toLowerCase() == "content-security-policy") { + ok(!found, "The CSP header only appears once"); + found = true; + const value = name.nextElementSibling.textContent; + const expected = "default-src 'none'; base-uri 'none';"; + is(value, expected, "The CSP value has not been altered"); + } + } + ok(found, "The CSP header is present"); + }); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_data_blocking.js b/devtools/client/jsonview/test/browser_jsonview_data_blocking.js new file mode 100644 index 0000000000..f36d39dcc8 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_data_blocking.js @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view"; +const nullP = Services.scriptSecurityManager.createNullPrincipal({}); + +// We need 3 levels of nesting just to get to run something against a content +// page, so the devtools limit of 4 levels of nesting don't help: +/* eslint max-nested-callbacks: 0 */ + +/** + * Check that we don't expose a JSONView object on data: URI windows where + * we block the load. + */ +add_task(async function test_blocked_data_exposure() { + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]], + }); + await BrowserTestUtils.withNewTab(TEST_PATH + "empty.html", async browser => { + const tabCount = gBrowser.tabs.length; + await SpecialPowers.spawn(browser, [], function () { + content.w = content.window.open( + "data:application/vnd.mozilla.json.view,1", + "_blank" + ); + ok( + !Cu.waiveXrays(content.w).JSONView, + "Should not have created a JSON View object" + ); + // We need to wait for the JSON view machinery to actually have a chance to run. + // We have no way to detect that it has or hasn't, so a setTimeout is the best we + // can do, unfortunately. + return new Promise(resolve => { + content.setTimeout(function () { + // Putting the resolve before the check to avoid JS errors potentially causing + // the test to time out. + resolve(); + ok( + !Cu.waiveXrays(content.w).JSONView, + "Should still not have a JSON View object" + ); + }, 1000); + }); + }); + // Without this, if somehow the data: protocol blocker stops working, the + // test would just keep passing. + is( + tabCount, + gBrowser.tabs.length, + "Haven't actually opened a new window/tab" + ); + }); +}); + +/** + * Check that aborted channels also abort sending data from the stream converter. + */ +add_task(async function test_converter_abort_should_stop_data_sending() { + const loadInfo = NetUtil.newChannel({ + uri: Services.io.newURI("data:text/plain,"), + loadingPrincipal: nullP, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }).loadInfo; + // Stub all the things. + const chan = { + QueryInterface: ChromeUtils.generateQI([ + "nsIChannel", + "nsIWritablePropertyBag", + ]), + URI: Services.io.newURI("data:application/json,{}"), + // loadinfo is builtinclass, need to actually have one: + loadInfo, + notificationCallbacks: { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + getInterface() { + // We want a loadcontext here, which is also builtinclass, can't stub. + return docShell; + }, + }, + status: Cr.NS_OK, + setProperty() {}, + }; + let onStartFired = false; + const listener = { + QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), + onStartRequest() { + onStartFired = true; + // This should force the converter to abort, too: + chan.status = Cr.NS_BINDING_ABORTED; + }, + onDataAvailable() { + ok(false, "onDataAvailable should never fire"); + }, + }; + const conv = Cc[ + "@mozilla.org/streamconv;1?from=" + JSON_VIEW_MIME_TYPE + "&to=*/*" + ].createInstance(Ci.nsIStreamConverter); + conv.asyncConvertData( + "application/vnd.mozilla.json.view", + "text/html", + listener, + null + ); + conv.onStartRequest(chan); + ok(onStartFired, "Should have fired onStartRequest"); +}); + +/** + * Check that principal mismatches break things. Note that we're stubbing + * the window associated with the channel to be a browser window; the + * converter should be bailing out because the window's principal won't + * match the null principal to which the converter tries to reset the + * inheriting principal of the channel. + */ +add_task(async function test_converter_principal_needs_matching() { + const loadInfo = NetUtil.newChannel({ + uri: Services.io.newURI("data:text/plain,"), + loadingPrincipal: nullP, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }).loadInfo; + // Stub all the things. + const chan = { + QueryInterface: ChromeUtils.generateQI([ + "nsIChannel", + "nsIWritablePropertyBag", + ]), + URI: Services.io.newURI("data:application/json,{}"), + // loadinfo is builtinclass, need to actually have one: + loadInfo, + notificationCallbacks: { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + getInterface() { + // We want a loadcontext here, which is also builtinclass, can't stub. + return docShell; + }, + }, + status: Cr.NS_OK, + setProperty() {}, + cancel(arg) { + this.status = arg; + }, + }; + let onStartFired = false; + const listener = { + QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), + onStartRequest() { + onStartFired = true; + }, + onDataAvailable() { + ok(false, "onDataAvailable should never fire"); + }, + }; + const conv = Cc[ + "@mozilla.org/streamconv;1?from=" + JSON_VIEW_MIME_TYPE + "&to=*/*" + ].createInstance(Ci.nsIStreamConverter); + conv.asyncConvertData( + "application/vnd.mozilla.json.view", + "text/html", + listener, + null + ); + conv.onStartRequest(chan); + ok(onStartFired, "Should have fired onStartRequest"); + is(chan.status, Cr.NS_BINDING_ABORTED, "Should have been aborted."); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_empty_object.js b/devtools/client/jsonview/test/browser_jsonview_empty_object.js new file mode 100644 index 0000000000..af3dfbc007 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_empty_object.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function testRootObject(objExpr, summary = objExpr) { + return async function () { + info("Test JSON with root empty object " + objExpr + " started"); + + const TEST_JSON_URL = "data:application/json," + objExpr; + await addJsonViewTab(TEST_JSON_URL); + + const objectText = await getElementText(".jsonPanelBox .panelContent"); + is(objectText, summary, "The root object " + objExpr + " is visible"); + }; +} + +function testNestedObject(objExpr, summary = objExpr) { + return async function () { + info("Test JSON with nested empty object " + objExpr + " started"); + + const TEST_JSON_URL = "data:application/json,[" + objExpr + "]"; + await addJsonViewTab(TEST_JSON_URL); + + const objectCellCount = await getElementCount( + ".jsonPanelBox .treeTable .objectCell" + ); + is(objectCellCount, 1, "There must be one object cell"); + + const objectCellText = await getElementText( + ".jsonPanelBox .treeTable .objectCell" + ); + is(objectCellText, summary, objExpr + " has a visible summary"); + + // Collapse auto-expanded node. + await clickJsonNode(".jsonPanelBox .treeTable .treeLabel"); + + const textAfter = await getElementText( + ".jsonPanelBox .treeTable .objectCell" + ); + is(textAfter, summary, objExpr + " still has a visible summary"); + }; +} + +add_task(testRootObject("null")); +add_task(testNestedObject("null")); +add_task(testNestedObject("[]")); +add_task(testNestedObject("{}")); diff --git a/devtools/client/jsonview/test/browser_jsonview_encoding.js b/devtools/client/jsonview/test/browser_jsonview_encoding.js new file mode 100644 index 0000000000..be49af39d2 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_encoding.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + info("Test JSON encoding started"); + + const bom = "%EF%BB%BF"; // UTF-8 BOM + const tests = [ + { + input: bom, + output: "", + }, + { + input: "%FE%FF", // UTF-16BE BOM + output: "\uFFFD\uFFFD", + }, + { + input: "%FF%FE", // UTF-16LE BOM + output: "\uFFFD\uFFFD", + }, + { + input: bom + "%30", + output: "0", + }, + { + input: bom + bom, + output: "\uFEFF", + }, + { + input: "%00%61", + output: "\u0000a", + }, + { + input: "%61%00", + output: "a\u0000", + }, + { + input: "%30%FF", + output: "0�", + }, + { + input: "%C3%A0", + output: "à", + }, + { + input: "%E2%9D%A4", + output: "❤", + }, + { + input: "%F0%9F%9A%80", + output: "🚀", + }, + ]; + + for (const { input, output } of tests) { + info("Test decoding of " + JSON.stringify(input) + "."); + + await addJsonViewTab("data:application/json," + input); + await selectJsonViewContentTab("rawdata"); + + // Check displayed data. + const data = await getElementText(".textPanelBox .data"); + is(data, output, "The right data has been received."); + } +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_expand_collapse.js b/devtools/client/jsonview/test/browser_jsonview_expand_collapse.js new file mode 100644 index 0000000000..6b681ff0cc --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_expand_collapse.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "jsonViewStrings", () => { + return Services.strings.createBundle( + "chrome://devtools/locale/jsonview.properties" + ); +}); + +const TEST_JSON_URL = URL_ROOT + "array_json.json"; +const EXPAND_THRESHOLD = 100 * 1024; + +add_task(async function () { + info("Test expand/collapse small JSON started"); + + await addJsonViewTab(TEST_JSON_URL); + + /* Initial sanity check */ + const countBefore = await getElementCount(".treeRow"); + is(countBefore, 6, "There must be six rows"); + + /* Test the "Collapse All" button */ + let selector = ".jsonPanelBox .toolbar button.collapse"; + await clickJsonNode(selector); + let countAfter = await getElementCount(".treeRow"); + is(countAfter, 3, "There must be three rows"); + + /* Test the "Expand All" button */ + selector = ".jsonPanelBox .toolbar button.expand"; + is( + await getElementText(selector), + jsonViewStrings.GetStringFromName("jsonViewer.ExpandAll"), + "Expand button doesn't warn that the action will be slow" + ); + await clickJsonNode(selector); + countAfter = await getElementCount(".treeRow"); + is(countAfter, 6, "There must be six expanded rows"); +}); + +add_task(async function () { + info("Test expand button for big JSON started"); + + const json = JSON.stringify({ + data: Array(1e5) + .fill() + .map(x => "hoot"), + status: "ok", + }); + ok( + json.length > EXPAND_THRESHOLD, + "The generated JSON must be larger than 100kB" + ); + await addJsonViewTab("data:application/json," + json); + is( + await getElementText(".jsonPanelBox .toolbar button.expand"), + jsonViewStrings.GetStringFromName("jsonViewer.ExpandAllSlow"), + "Expand button warns that the action will be slow" + ); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_filter.js b/devtools/client/jsonview/test/browser_jsonview_filter.js new file mode 100644 index 0000000000..1dd093da5b --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_filter.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "array_json.json"; + +add_task(async function () { + info("Test valid JSON started"); + + await addJsonViewTab(TEST_JSON_URL); + + const count = await getElementCount(".jsonPanelBox .treeTable .treeRow"); + is(count, 6, "There must be expected number of rows"); + + // XXX use proper shortcut to focus the filter box + // as soon as bug Bug 1178771 is fixed. + await sendString("h", ".jsonPanelBox .searchBox"); + + // The filtering is done asynchronously so, we need to wait. + await waitForFilter(); + + const hiddenCount = await getElementCount( + ".jsonPanelBox .treeTable .treeRow.hidden" + ); + is(hiddenCount, 4, "There must be expected number of hidden rows"); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_filter_clear.js b/devtools/client/jsonview/test/browser_jsonview_filter_clear.js new file mode 100644 index 0000000000..70ece7ee03 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_filter_clear.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "array_json.json"; +const filterClearButton = "button.devtools-searchinput-clear"; + +function clickAndWaitForFilterClear(selector) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], sel => { + content.document.querySelector(sel).click(); + + const rows = Array.from( + content.document.querySelectorAll(".jsonPanelBox .treeTable .treeRow") + ); + + // If there are no hiddens rows + if (!rows.some(r => r.classList.contains("hidden"))) { + info("The view has been cleared, no need for mutationObserver"); + return Promise.resolve(); + } + + return new Promise(resolve => { + // Wait until we have 0 hidden rows + const observer = new content.MutationObserver(function (mutations) { + info("New mutation ! "); + info(`${mutations}`); + for (let i = 0; i < mutations.length; i++) { + const mutation = mutations[i]; + info(`type: ${mutation.type}`); + info(`attributeName: ${mutation.attributeName}`); + info(`attributeNamespace: ${mutation.attributeNamespace}`); + info(`oldValue: ${mutation.oldValue}`); + if (mutation.attributeName == "class") { + if (!rows.some(r => r.classList.contains("hidden"))) { + info("resolving mutationObserver"); + observer.disconnect(); + resolve(); + break; + } + } + } + }); + + const parent = content.document.querySelector("tbody"); + observer.observe(parent, { attributes: true, subtree: true }); + }); + }); +} + +add_task(async function () { + info("Test filter input is cleared when pressing the clear button"); + + await addJsonViewTab(TEST_JSON_URL); + + // Type "honza" in the filter input + const count = await getElementCount(".jsonPanelBox .treeTable .treeRow"); + is(count, 6, "There must be expected number of rows"); + await sendString("honza", ".jsonPanelBox .searchBox"); + await waitForFilter(); + + const filterInputValue = await getFilterInputValue(); + is(filterInputValue, "honza", "Filter input shoud be filled"); + + // Check the json is filtered + const hiddenCount = await getElementCount( + ".jsonPanelBox .treeTable .treeRow.hidden" + ); + is(hiddenCount, 4, "There must be expected number of hidden rows"); + + info("Click on the close button"); + await clickAndWaitForFilterClear(filterClearButton); + + // Check the json is not filtered and the filter input is empty + const newfilterInputValue = await getFilterInputValue(); + is(newfilterInputValue, "", "Filter input should be empty"); + const newCount = await getElementCount( + ".jsonPanelBox .treeTable .treeRow.hidden" + ); + is(newCount, 0, "There must be expected number of rows"); +}); + +function getFilterInputValue() { + return getElementAttr(".devtools-filterinput", "value"); +} diff --git a/devtools/client/jsonview/test/browser_jsonview_ignore_charset.js b/devtools/client/jsonview/test/browser_jsonview_ignore_charset.js new file mode 100644 index 0000000000..ffd00a35e3 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_ignore_charset.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + info("Test ignored charset parameter started"); + + const encodedChar = "%E2%9D%A4"; // In UTF-8 this is a heavy black heart + const result = "❤"; + const TEST_JSON_URL = "data:application/json;charset=ANSI," + encodedChar; + + await addJsonViewTab(TEST_JSON_URL); + await selectJsonViewContentTab("rawdata"); + + const text = await getElementText(".textPanelBox .data"); + is(text, result, "The charset parameter is ignored and UTF-8 is used."); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_initial_focus.js b/devtools/client/jsonview/test/browser_jsonview_initial_focus.js new file mode 100644 index 0000000000..71b0623ce5 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_initial_focus.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const VALID_JSON_URL = URL_ROOT + "valid_json.json"; + +add_task(async function () { + info("Test focus JSON view started"); + await addJsonViewTab(VALID_JSON_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const scroller = content.document.getElementById("json-scrolling-panel"); + ok(scroller, "Found the scrollable area"); + is( + content.document.activeElement, + scroller, + "Scrollable area initially focused" + ); + }); + await reloadBrowser(); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const scroller = await ContentTaskUtils.waitForCondition( + () => content.document.getElementById("json-scrolling-panel"), + "Wait for the panel to be loaded" + ); + is( + content.document.activeElement, + scroller, + "Scrollable area focused after reload" + ); + }); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_invalid_json.js b/devtools/client/jsonview/test/browser_jsonview_invalid_json.js new file mode 100644 index 0000000000..461030959b --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_invalid_json.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "invalid_json.json"; + +add_task(async function () { + info("Test invalid JSON started"); + + await addJsonViewTab(TEST_JSON_URL); + + const count = await getElementCount(".jsonPanelBox .treeTable .treeRow"); + ok(count == 0, "There must be no row"); + + const text = await getElementText(".jsonPanelBox .jsonParseError"); + ok(text, "There must be an error description"); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_manifest.js b/devtools/client/jsonview/test/browser_jsonview_manifest.js new file mode 100644 index 0000000000..8072324cf1 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_manifest.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "manifest_json.json"; + +add_task(async function () { + info("Test manifest JSON file started"); + + await addJsonViewTab(TEST_JSON_URL); + + const count = await getElementCount(".jsonPanelBox .treeTable .treeRow"); + is(count, 37, "There must be expected number of rows"); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_nojs.js b/devtools/client/jsonview/test/browser_jsonview_nojs.js new file mode 100644 index 0000000000..38459617a2 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_nojs.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + info("Test JSON without JavaScript started."); + + const oldPref = Services.prefs.getBoolPref("javascript.enabled"); + Services.prefs.setBoolPref("javascript.enabled", false); + + const TEST_JSON_URL = "data:application/json,[1,2,3]"; + + // "uninitialized" will be the last app readyState because JS is disabled. + await addJsonViewTab(TEST_JSON_URL, { appReadyState: "uninitialized" }); + + info("Checking visible text contents."); + + const text = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const element = content.document.querySelector("html"); + return element ? element.innerText : null; + }); + is(text, "[1,2,3]", "The raw source should be visible."); + + Services.prefs.setBoolPref("javascript.enabled", oldPref); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_nul.js b/devtools/client/jsonview/test/browser_jsonview_nul.js new file mode 100644 index 0000000000..746846796b --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_nul.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + info("Test JSON with NUL started."); + + const TEST_JSON_URL = 'data:application/json,"foo_%00_bar"'; + await addJsonViewTab(TEST_JSON_URL); + + await selectJsonViewContentTab("rawdata"); + const rawData = await getElementText(".textPanelBox .data"); + is(rawData, '"foo_\u0000_bar"', "The NUL character has been preserved."); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_object-type.js b/devtools/client/jsonview/test/browser_jsonview_object-type.js new file mode 100644 index 0000000000..08b9ed865c --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_object-type.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ELLIPSIS } = require("resource://devtools/shared/l10n.js"); + +add_task(async function () { + info("Test Object type property started"); + + const TEST_JSON_URL = 'data:application/json,{"x":{"type":"string"}}'; + await addJsonViewTab(TEST_JSON_URL); + + let count = await getElementCount(".jsonPanelBox .treeTable .treeRow"); + is(count, 2, "There must be two rows"); + + // Collapse auto-expanded node. + await clickJsonNode(".jsonPanelBox .treeTable .treeLabel"); + + count = await getElementCount(".jsonPanelBox .treeTable .treeRow"); + is(count, 1, "There must be one row"); + + const label = await getElementText(".jsonPanelBox .treeTable .objectCell"); + is(label, `{${ELLIPSIS}}`, "The label must be indicating an object"); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_row_selection.js b/devtools/client/jsonview/test/browser_jsonview_row_selection.js new file mode 100644 index 0000000000..300ac40a03 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_row_selection.js @@ -0,0 +1,245 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + info("Test 1 JSON row selection started"); + + // Create a tall JSON so that there is a scrollbar. + const numRows = 1e3; + const json = JSON.stringify( + Array(numRows) + .fill() + .map((_, i) => i) + ); + const tab = await addJsonViewTab("data:application/json," + json); + + is( + await getElementCount(".treeRow"), + numRows, + "Got the expected number of rows." + ); + await assertRowSelected(null); + + // Focus the tree and select first row. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const tree = content.document.querySelector(".treeTable"); + tree.focus(); + is(tree, content.document.activeElement, "Tree should be focused"); + content.document.querySelector(".treeRow:nth-child(1)").click(); + }); + await assertRowSelected(1); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + ok(scroller.clientHeight < scroller.scrollHeight, "There is a scrollbar."); + is(scroller.scrollTop, 0, "Initially scrolled to the top."); + }); + + // Select last row. + await BrowserTestUtils.synthesizeKey("VK_END", {}, tab.linkedBrowser); + await assertRowSelected(numRows); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + is( + scroller.scrollTop + scroller.clientHeight, + scroller.scrollHeight, + "Scrolled to the bottom." + ); + // Click to select 2nd row. + content.document.querySelector(".treeRow:nth-child(2)").click(); + }); + await assertRowSelected(2); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + ok(scroller.scrollTop > 0, "Not scrolled to the top."); + // Synthesize up arrow key to select first row. + content.document.querySelector(".treeTable").focus(); + }); + await BrowserTestUtils.synthesizeKey("VK_UP", {}, tab.linkedBrowser); + await assertRowSelected(1); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + is(scroller.scrollTop, 0, "Scrolled to the top."); + }); +}); + +add_task(async function () { + info("Test 2 JSON row selection started"); + + const numRows = 4; + const tab = await addJsonViewTab("data:application/json,[0,1,2,3]"); + + is( + await getElementCount(".treeRow"), + numRows, + "Got the expected number of rows." + ); + await assertRowSelected(null); + + // Click to select first row. + await clickJsonNode(".treeRow:first-child"); + await assertRowSelected(1); + + // Synthesize multiple down arrow keydowns to select following rows. + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + content.document.querySelector(".treeTable").focus(); + }); + for (let i = 2; i < numRows; ++i) { + await BrowserTestUtils.synthesizeKey( + "VK_DOWN", + { type: "keydown" }, + tab.linkedBrowser + ); + await assertRowSelected(i); + } + + // Now synthesize the keyup, this shouldn't change selected row. + await BrowserTestUtils.synthesizeKey( + "VK_DOWN", + { type: "keyup" }, + tab.linkedBrowser + ); + await wait(500); + await assertRowSelected(numRows - 1); + + // Finally, synthesize keydown with a modifier, this also shouldn't change selected row. + await BrowserTestUtils.synthesizeKey( + "VK_DOWN", + { type: "keydown", shiftKey: true }, + tab.linkedBrowser + ); + await wait(500); + await assertRowSelected(numRows - 1); +}); + +add_task(async function () { + info("Test 3 JSON row selection started"); + + // Create a JSON with a row taller than the panel. + const json = JSON.stringify([0, "a ".repeat(1e4), 1]); + const tab = await addJsonViewTab("data:application/json," + encodeURI(json)); + + is(await getElementCount(".treeRow"), 3, "Got the expected number of rows."); + await assertRowSelected(null); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + const row = content.document.querySelector(".treeRow:nth-child(2)"); + ok( + scroller.clientHeight < row.clientHeight, + "The row is taller than the scroller." + ); + is(scroller.scrollTop, 0, "Initially scrolled to the top."); + + // Select the tall row. + content.document.querySelector(".treeTable").focus(); + row.click(); + }); + await assertRowSelected(2); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + is( + scroller.scrollTop, + 0, + "When the row is visible, do not scroll on click." + ); + }); + + // Select the last row. + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, tab.linkedBrowser); + await assertRowSelected(3); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + is( + scroller.scrollTop + scroller.offsetHeight, + scroller.scrollHeight, + "Scrolled to the bottom." + ); + + // Select the tall row. + const row = content.document.querySelector(".treeRow:nth-child(2)"); + row.click(); + }); + + await assertRowSelected(2); + const scroll = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + function () { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + const row = content.document.querySelector(".treeRow:nth-child(2)"); + is( + scroller.scrollTop + scroller.offsetHeight, + scroller.scrollHeight, + "Scrolled to the bottom. When the row is visible, do not scroll on click." + ); + + // Scroll up a bit, so that both the top and bottom of the row are not visible. + const scrollPos = (scroller.scrollTop = Math.ceil( + (scroller.scrollTop + row.offsetTop) / 2 + )); + ok( + scroller.scrollTop > row.offsetTop, + "The top of the row is not visible." + ); + ok( + scroller.scrollTop + scroller.offsetHeight < + row.offsetTop + row.offsetHeight, + "The bottom of the row is not visible." + ); + + // Select the tall row. + row.click(); + return scrollPos; + } + ); + + await assertRowSelected(2); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [scroll], + function (scrollPos) { + const scroller = content.document.querySelector( + ".jsonPanelBox .panelContent" + ); + is(scroller.scrollTop, scrollPos, "Scroll did not change"); + } + ); +}); + +async function assertRowSelected(rowNum) { + const idx = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + function () { + return [].indexOf.call( + content.document.querySelectorAll(".treeRow"), + content.document.querySelector(".treeRow.selected") + ); + } + ); + is( + idx + 1, + +rowNum, + `${rowNum ? "The row #" + rowNum : "No row"} is selected.` + ); +} diff --git a/devtools/client/jsonview/test/browser_jsonview_save_json.js b/devtools/client/jsonview/test/browser_jsonview_save_json.js new file mode 100644 index 0000000000..506967df00 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_save_json.js @@ -0,0 +1,159 @@ +/* eslint-disable no-unused-vars, no-undef */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const saveButton = "button.save"; +const prettifyButton = "button.prettyprint"; + +const { MockFilePicker } = SpecialPowers; +MockFilePicker.init(window); +MockFilePicker.returnValue = MockFilePicker.returnOK; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +function awaitSavedFileContents(name, ext) { + return new Promise((resolve, reject) => { + MockFilePicker.showCallback = fp => { + try { + ok(true, "File picker was opened"); + const fileName = fp.defaultString; + is( + fileName, + name, + "File picker should provide the correct default filename." + ); + is( + fp.defaultExtension, + ext, + "File picker should provide the correct default file extension." + ); + const destFile = destDir.clone(); + destFile.append(fileName); + MockFilePicker.setFiles([destFile]); + MockFilePicker.showCallback = null; + mockTransferCallback = async function (downloadSuccess) { + try { + ok( + downloadSuccess, + "JSON should have been downloaded successfully" + ); + ok(destFile.exists(), "The downloaded file should exist."); + const { path } = destFile; + await BrowserTestUtils.waitForCondition(() => IOUtils.exists(path)); + await BrowserTestUtils.waitForCondition(async () => { + const { size } = await IOUtils.stat(path); + return size > 0; + }); + const buffer = await IOUtils.read(path); + resolve(new TextDecoder().decode(buffer)); + } catch (error) { + reject(error); + } + }; + } catch (error) { + reject(error); + } + }; + }); +} + +function createTemporarySaveDirectory() { + const saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("jsonview-testsavedir"); + if (!saveDir.exists()) { + info("Creating temporary save directory."); + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + info("Temporary save directory: " + saveDir.path); + return saveDir; +} + +const destDir = createTemporarySaveDirectory(); +mockTransferRegisterer.register(); +MockFilePicker.displayDirectory = destDir; +registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + destDir.remove(true); + ok(!destDir.exists(), "Destination dir should be removed"); +}); + +add_task(async function () { + info("Test 1 save JSON started"); + + const JSON_FILE = "simple_json.json"; + const TEST_JSON_URL = URL_ROOT + JSON_FILE; + const tab = await addJsonViewTab(TEST_JSON_URL); + + const response = await fetch(new Request(TEST_JSON_URL)); + + info("Fetched JSON contents."); + const rawJSON = await response.text(); + const prettyJSON = JSON.stringify(JSON.parse(rawJSON), null, " "); + isnot( + rawJSON, + prettyJSON, + "Original and prettified JSON should be different." + ); + + // Attempt to save original JSON via saveBrowser (ctrl/cmd+s or "Save Page As" command). + let data = awaitSavedFileContents(JSON_FILE, "json"); + saveBrowser(tab.linkedBrowser); + is(await data, rawJSON, "Original JSON contents should have been saved."); + + // Attempt to save original JSON via "Save" button + data = awaitSavedFileContents(JSON_FILE, "json"); + await clickJsonNode(saveButton); + info("Clicked Save button."); + is(await data, rawJSON, "Original JSON contents should have been saved."); + + // Attempt to save prettified JSON via "Save" button + await selectJsonViewContentTab("rawdata"); + info("Switched to Raw Data tab."); + await clickJsonNode(prettifyButton); + info("Clicked Pretty Print button."); + data = awaitSavedFileContents(JSON_FILE, "json"); + await clickJsonNode(saveButton); + info("Clicked Save button."); + is( + await data, + prettyJSON, + "Prettified JSON contents should have been saved." + ); + + // saveBrowser should still save original contents. + data = awaitSavedFileContents(JSON_FILE, "json"); + saveBrowser(tab.linkedBrowser); + is(await data, rawJSON, "Original JSON contents should have been saved."); +}); + +add_task(async function () { + info("Test 2 save JSON started"); + + const TEST_JSON_URL = "data:application/json,2"; + await addJsonViewTab(TEST_JSON_URL); + + info("Checking that application/json adds .json extension by default."); + const data = awaitSavedFileContents("Untitled.json", "json"); + await clickJsonNode(saveButton); + info("Clicked Save button."); + is(await data, "2", "JSON contents should have been saved."); +}); + +add_task(async function () { + info("Test 3 save JSON started"); + + const TEST_JSON_URL = "data:application/manifest+json,3"; + await addJsonViewTab(TEST_JSON_URL); + + info("Checking that application/manifest+json does not add .json extension."); + const data = awaitSavedFileContents("Untitled", null); + await clickJsonNode(saveButton); + info("Clicked Save button."); + is(await data, "3", "JSON contents should have been saved."); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_serviceworker.js b/devtools/client/jsonview/test/browser_jsonview_serviceworker.js new file mode 100644 index 0000000000..37c407ecd4 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_serviceworker.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT_SSL + "valid_json.json"; +const EMPTY_PAGE = URL_ROOT_SSL + "empty.html"; +const SW = URL_ROOT_SSL + "passthrough-sw.js"; + +add_task(async function () { + info("Test valid JSON with service worker started"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + const swTab = BrowserTestUtils.addTab(gBrowser, EMPTY_PAGE); + const browser = gBrowser.getBrowserForTab(swTab); + await BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn( + browser, + [{ script: SW, scope: TEST_JSON_URL }], + async opts => { + const reg = await content.navigator.serviceWorker.register(opts.script, { + scope: opts.scope, + }); + return new content.window.Promise(resolve => { + const worker = reg.installing; + if (worker.state === "activated") { + resolve(); + return; + } + worker.addEventListener("statechange", evt => { + if (worker.state === "activated") { + resolve(); + } + }); + }); + } + ); + + const tab = await addJsonViewTab(TEST_JSON_URL); + + ok( + tab.linkedBrowser.contentPrincipal.isNullPrincipal, + "Should have null principal" + ); + + is(await countRows(), 3, "There must be three rows"); + + const objectCellCount = await getElementCount( + ".jsonPanelBox .treeTable .objectCell" + ); + is(objectCellCount, 1, "There must be one object cell"); + + const objectCellText = await getElementText( + ".jsonPanelBox .treeTable .objectCell" + ); + is(objectCellText, "", "The summary is hidden when object is expanded"); + + // Clicking the value does not collapse it (so that it can be selected and copied). + await clickJsonNode(".jsonPanelBox .treeTable .treeValueCell"); + is(await countRows(), 3, "There must still be three rows"); + + // Clicking the label collapses the auto-expanded node. + await clickJsonNode(".jsonPanelBox .treeTable .treeLabel"); + is(await countRows(), 1, "There must be one row"); + + await SpecialPowers.spawn( + browser, + [{ script: SW, scope: TEST_JSON_URL }], + async opts => { + const reg = await content.navigator.serviceWorker.getRegistration( + opts.scope + ); + await reg.unregister(); + } + ); + + BrowserTestUtils.removeTab(swTab); +}); + +function countRows() { + return getElementCount(".jsonPanelBox .treeTable .treeRow"); +} diff --git a/devtools/client/jsonview/test/browser_jsonview_slash.js b/devtools/client/jsonview/test/browser_jsonview_slash.js new file mode 100644 index 0000000000..f8abeca7be --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_slash.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + info("Test JSON with slash started."); + + const TEST_JSON_URL = 'data:application/json,{"a/b":[1,2],"a":{"b":[3,4]}}'; + await addJsonViewTab(TEST_JSON_URL); + + const countBefore = await getElementCount( + ".jsonPanelBox .treeTable .treeRow" + ); + is(countBefore, 7, "There must be seven rows"); +}); diff --git a/devtools/client/jsonview/test/browser_jsonview_theme.js b/devtools/client/jsonview/test/browser_jsonview_theme.js new file mode 100644 index 0000000000..c66d197256 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_theme.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "valid_json.json"; + +add_task(async function () { + info("Test JSON theme started."); + + const oldPref = Services.prefs.getCharPref("devtools.theme"); + Services.prefs.setCharPref("devtools.theme", "light"); + + await addJsonViewTab(TEST_JSON_URL); + + is(await getTheme(), "theme-light", "The initial theme is light"); + + Services.prefs.setCharPref("devtools.theme", "dark"); + is(await getTheme(), "theme-dark", "Theme changed to dark"); + + Services.prefs.setCharPref("devtools.theme", "light"); + is(await getTheme(), "theme-light", "Theme changed to light"); + + Services.prefs.setCharPref("devtools.theme", oldPref); +}); + +function getTheme() { + return getElementAttr(":root", "class"); +} diff --git a/devtools/client/jsonview/test/browser_jsonview_url_linkification.js b/devtools/client/jsonview/test/browser_jsonview_url_linkification.js new file mode 100644 index 0000000000..19d1f27f0c --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_url_linkification.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ELLIPSIS } = require("resource://devtools/shared/l10n.js"); + +add_task(async function () { + info("Test short URL linkification JSON started"); + + const url = "https://example.com/"; + const tab = await addJsonViewTab( + "data:application/json," + JSON.stringify([url]) + ); + await testLinkNavigation({ browser: tab.linkedBrowser, url }); + + info("Switch back to the JSONViewer"); + await BrowserTestUtils.switchTab(gBrowser, tab); + + await testLinkNavigation({ + browser: tab.linkedBrowser, + url, + clickLabel: true, + }); +}); + +add_task(async function () { + info("Test long URL linkification JSON started"); + + const url = "https://example.com/" + "a".repeat(100); + const tab = await addJsonViewTab( + "data:application/json," + JSON.stringify([url]) + ); + + await testLinkNavigation({ browser: tab.linkedBrowser, url }); + + info("Switch back to the JSONViewer"); + await BrowserTestUtils.switchTab(gBrowser, tab); + + await testLinkNavigation({ + browser: tab.linkedBrowser, + url, + urlText: url.slice(0, 24) + ELLIPSIS + url.slice(-24), + clickLabel: true, + }); +}); + +/** + * Assert that the expected link is displayed and that clicking on it navigates to the + * expected url. + * + * @param {Object} option object containing: + * - browser (mandatory): the browser the tab will be opened in. + * - url (mandatory): The url we should navigate to. + * - urlText: The expected displayed text of the url. + * Falls back to `url` if not filled + * - clickLabel: Should we click the label before doing assertions. + */ +async function testLinkNavigation({ + browser, + url, + urlText, + clickLabel = false, +}) { + const onTabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, url); + + SpecialPowers.spawn(browser, [[urlText || url, url, clickLabel]], args => { + const [expectedURLText, expectedURL, shouldClickLabel] = args; + const { document } = content; + + if (shouldClickLabel === true) { + document.querySelector(".jsonPanelBox .treeTable .treeLabel").click(); + } + + const link = document.querySelector( + ".jsonPanelBox .treeTable .treeValueCell a" + ); + is(link.textContent, expectedURLText, "The expected URL is displayed."); + is(link.href, expectedURL, "The URL was linkified."); + + link.click(); + }); + + const newTab = await onTabLoaded; + // We only need to check that newTab is truthy since + // BrowserTestUtils.waitForNewTab checks the URL. + ok(newTab, "The expected tab was opened."); +} diff --git a/devtools/client/jsonview/test/browser_jsonview_valid_json.js b/devtools/client/jsonview/test/browser_jsonview_valid_json.js new file mode 100644 index 0000000000..8ffd1dc1d5 --- /dev/null +++ b/devtools/client/jsonview/test/browser_jsonview_valid_json.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_JSON_URL = URL_ROOT + "valid_json.json"; + +add_task(async function () { + info("Test valid JSON started"); + + const tab = await addJsonViewTab(TEST_JSON_URL); + + ok( + tab.linkedBrowser.contentPrincipal.isNullPrincipal, + "Should have null principal" + ); + + is(await countRows(), 3, "There must be three rows"); + + const objectCellCount = await getElementCount( + ".jsonPanelBox .treeTable .objectCell" + ); + is(objectCellCount, 1, "There must be one object cell"); + + const objectCellText = await getElementText( + ".jsonPanelBox .treeTable .objectCell" + ); + is(objectCellText, "", "The summary is hidden when object is expanded"); + + // Clicking the value does not collapse it (so that it can be selected and copied). + await clickJsonNode(".jsonPanelBox .treeTable .treeValueCell"); + is(await countRows(), 3, "There must still be three rows"); + + // Clicking the label collapses the auto-expanded node. + await clickJsonNode(".jsonPanelBox .treeTable .treeLabel"); + is(await countRows(), 1, "There must be one row"); + + // Collapsed nodes are preserved when switching panels. + await selectJsonViewContentTab("headers"); + await selectJsonViewContentTab("json"); + is(await countRows(), 1, "There must still be one row"); +}); + +function countRows() { + return getElementCount(".jsonPanelBox .treeTable .treeRow"); +} diff --git a/devtools/client/jsonview/test/chunked_json.sjs b/devtools/client/jsonview/test/chunked_json.sjs new file mode 100644 index 0000000000..554b62bafd --- /dev/null +++ b/devtools/client/jsonview/test/chunked_json.sjs @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const key = "json-viewer-chunked-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) { + response.processAsync(); + setResponse(response); + response.setHeader("Content-Type", "application/json"); + // Write something so that the JSON viewer app starts loading. + response.write(" "); + return; + } + const [command, value] = queryString.split("="); + switch (command) { + case "write": + getResponse().write(value); + break; + case "close": + getResponse().finish(); + setResponse(null); + break; + } + response.setHeader("Content-Type", "text/plain"); + response.write("ok"); +} diff --git a/devtools/client/jsonview/test/csp_json.json b/devtools/client/jsonview/test/csp_json.json new file mode 100644 index 0000000000..9fc9368255 --- /dev/null +++ b/devtools/client/jsonview/test/csp_json.json @@ -0,0 +1 @@ +{ "csp": true } diff --git a/devtools/client/jsonview/test/csp_json.json^headers^ b/devtools/client/jsonview/test/csp_json.json^headers^ new file mode 100644 index 0000000000..d6d0a72a4b --- /dev/null +++ b/devtools/client/jsonview/test/csp_json.json^headers^ @@ -0,0 +1,2 @@ +Content-Type: application/json +Content-Security-Policy: default-src 'none'; base-uri 'none'; diff --git a/devtools/client/jsonview/test/empty.html b/devtools/client/jsonview/test/empty.html new file mode 100644 index 0000000000..c50eddd41f --- /dev/null +++ b/devtools/client/jsonview/test/empty.html @@ -0,0 +1 @@ +<!doctype html> diff --git a/devtools/client/jsonview/test/head.js b/devtools/client/jsonview/test/head.js new file mode 100644 index 0000000000..5c9ee85ea2 --- /dev/null +++ b/devtools/client/jsonview/test/head.js @@ -0,0 +1,278 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */ + +"use strict"; + +// shared-head.js handles imports, constants, and utility functions +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/head.js", + this +); + +const JSON_VIEW_PREF = "devtools.jsonview.enabled"; + +// Enable JSON View for the test +Services.prefs.setBoolPref(JSON_VIEW_PREF, true); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref(JSON_VIEW_PREF); +}); + +// XXX move some API into devtools/shared/test/shared-head.js + +/** + * Add a new test tab in the browser and load the given url. + * @param {String} url + * The url to be loaded in the new tab. + * + * @param {Object} [optional] + * An object with the following optional properties: + * - appReadyState: The readyState of the JSON Viewer app that you want to + * wait for. Its value can be one of: + * - "uninitialized": The converter has started the request. + * If JavaScript is disabled, there will be no more readyState changes. + * - "loading": RequireJS started loading the scripts for the JSON Viewer. + * If the load timeouts, there will be no more readyState changes. + * - "interactive": The JSON Viewer app loaded, but possibly not all the JSON + * data has been received. + * - "complete" (default): The app is fully loaded with all the JSON. + * - docReadyState: The standard readyState of the document that you want to + * wait for. Its value can be one of: + * - "loading": The JSON data has not been completely loaded (but the app might). + * - "interactive": All the JSON data has been received. + * - "complete" (default): Since there aren't sub-resources like images, + * behaves as "interactive". Note the app might not be loaded yet. + */ +async function addJsonViewTab( + url, + { appReadyState = "complete", docReadyState = "complete" } = {} +) { + info("Adding a new JSON tab with URL: '" + url + "'"); + const tabAdded = BrowserTestUtils.waitForNewTab(gBrowser, url); + const tabLoaded = addTab(url); + + // The `tabAdded` promise resolves when the JSON Viewer starts loading. + // This is usually what we want, however, it never resolves for unrecognized + // content types that trigger a download. + // On the other hand, `tabLoaded` always resolves, but not until the document + // is fully loaded, which is too late if `docReadyState !== "complete"`. + // Therefore, we race both promises. + const tab = await Promise.race([tabAdded, tabLoaded]); + const browser = tab.linkedBrowser; + + const rootDir = getRootDirectory(gTestPath); + + // Catch RequireJS errors (usually timeouts) + const error = tabLoaded.then(() => + SpecialPowers.spawn(browser, [], function () { + return new Promise((resolve, reject) => { + const { requirejs } = content.wrappedJSObject; + if (requirejs) { + requirejs.onError = err => { + info(err); + ok(false, "RequireJS error"); + reject(err); + }; + } + }); + }) + ); + + const data = { rootDir, appReadyState, docReadyState }; + await Promise.race([ + error, + // eslint-disable-next-line no-shadow + ContentTask.spawn(browser, data, async function (data) { + // Check if there is a JSONView object. + const { JSONView } = content.wrappedJSObject; + if (!JSONView) { + throw new Error("The JSON Viewer did not load."); + } + + const docReadyStates = ["loading", "interactive", "complete"]; + const docReadyIndex = docReadyStates.indexOf(data.docReadyState); + const appReadyStates = ["uninitialized", ...docReadyStates]; + const appReadyIndex = appReadyStates.indexOf(data.appReadyState); + if (docReadyIndex < 0 || appReadyIndex < 0) { + throw new Error("Invalid app or doc readyState parameter."); + } + + // Wait until the document readyState suffices. + const { document } = content; + while (docReadyStates.indexOf(document.readyState) < docReadyIndex) { + info( + `DocReadyState is "${document.readyState}". Await "${data.docReadyState}"` + ); + await new Promise(resolve => { + document.addEventListener("readystatechange", resolve, { + once: true, + }); + }); + } + + // Wait until the app readyState suffices. + while (appReadyStates.indexOf(JSONView.readyState) < appReadyIndex) { + info( + `AppReadyState is "${JSONView.readyState}". Await "${data.appReadyState}"` + ); + await new Promise(resolve => { + content.addEventListener("AppReadyStateChange", resolve, { + once: true, + }); + }); + } + }), + ]); + + return tab; +} + +/** + * Expanding a node in the JSON tree + */ +function clickJsonNode(selector) { + info("Expanding node: '" + selector + "'"); + + // eslint-disable-next-line no-shadow + return ContentTask.spawn(gBrowser.selectedBrowser, selector, selector => { + content.document.querySelector(selector).click(); + }); +} + +/** + * Select JSON View tab (in the content). + */ +function selectJsonViewContentTab(name) { + info("Selecting tab: '" + name + "'"); + + // eslint-disable-next-line no-shadow + return ContentTask.spawn(gBrowser.selectedBrowser, name, async name => { + const selector = ".tabs-menu .tabs-menu-item." + CSS.escape(name) + " a"; + const element = content.document.querySelector(selector); + is(element.getAttribute("aria-selected"), "false", "Tab not selected yet"); + await new Promise(resolve => { + content.addEventListener("TabChanged", resolve, { once: true }); + element.click(); + }); + is(element.getAttribute("aria-selected"), "true", "Tab is now selected"); + }); +} + +function getElementCount(selector) { + info("Get element count: '" + selector + "'"); + + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector], + selectorChild => { + return content.document.querySelectorAll(selectorChild).length; + } + ); +} + +function getElementText(selector) { + info("Get element text: '" + selector + "'"); + + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector], + selectorChild => { + const element = content.document.querySelector(selectorChild); + return element ? element.textContent : null; + } + ); +} + +function getElementAttr(selector, attr) { + info("Get attribute '" + attr + "' for element '" + selector + "'"); + + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, attr], + (selectorChild, attrChild) => { + const element = content.document.querySelector(selectorChild); + return element ? element.getAttribute(attrChild) : null; + } + ); +} + +function focusElement(selector) { + info("Focus element: '" + selector + "'"); + + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector], + selectorChild => { + const element = content.document.querySelector(selectorChild); + if (element) { + element.focus(); + } + } + ); +} + +/** + * Send the string aStr to the focused element. + * + * For now this method only works for ASCII characters and emulates the shift + * key state on US keyboard layout. + */ +function sendString(str, selector) { + info("Send string: '" + str + "'"); + + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [selector, str], + (selectorChild, strChild) => { + if (selectorChild) { + const element = content.document.querySelector(selectorChild); + if (element) { + element.focus(); + } + } + + EventUtils.sendString(strChild, content); + } + ); +} + +function waitForTime(delay) { + return new Promise(resolve => setTimeout(resolve, delay)); +} + +function waitForFilter() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return new Promise(resolve => { + const firstRow = content.document.querySelector( + ".jsonPanelBox .treeTable .treeRow" + ); + + // Check if the filter is already set. + if (firstRow.classList.contains("hidden")) { + resolve(); + return; + } + + // Wait till the first row has 'hidden' class set. + const observer = new content.MutationObserver(function (mutations) { + for (let i = 0; i < mutations.length; i++) { + const mutation = mutations[i]; + if (mutation.attributeName == "class") { + if (firstRow.classList.contains("hidden")) { + observer.disconnect(); + resolve(); + break; + } + } + } + }); + + observer.observe(firstRow, { attributes: true }); + }); + }); +} + +function normalizeNewLines(value) { + return value.replace("(\r\n|\n)", "\n"); +} diff --git a/devtools/client/jsonview/test/invalid_json.json b/devtools/client/jsonview/test/invalid_json.json new file mode 100644 index 0000000000..004e1e2032 --- /dev/null +++ b/devtools/client/jsonview/test/invalid_json.json @@ -0,0 +1 @@ +{,} diff --git a/devtools/client/jsonview/test/invalid_json.json^headers^ b/devtools/client/jsonview/test/invalid_json.json^headers^ new file mode 100644 index 0000000000..6010bfd188 --- /dev/null +++ b/devtools/client/jsonview/test/invalid_json.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json; charset=utf-8 diff --git a/devtools/client/jsonview/test/manifest_json.json b/devtools/client/jsonview/test/manifest_json.json new file mode 100644 index 0000000000..b178f7acf4 --- /dev/null +++ b/devtools/client/jsonview/test/manifest_json.json @@ -0,0 +1,49 @@ +{ + "name": "HackerWeb", + "short_name": "HackerWeb", + "start_url": ".", + "display": "standalone", + "background_color": "#fff", + "description": "A simply readable Hacker News app.", + "icons": [ + { + "src": "images/touch/homescreen48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "images/touch/homescreen72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "images/touch/homescreen96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "images/touch/homescreen144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "images/touch/homescreen168.png", + "sizes": "168x168", + "type": "image/png" + }, + { + "src": "images/touch/homescreen192.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "related_applications": [ + { + "platform": "web" + }, + { + "platform": "play", + "url": "https://play.google.com/store/apps/details?id=cheeaun.hackerweb" + } + ] +} diff --git a/devtools/client/jsonview/test/manifest_json.json^headers^ b/devtools/client/jsonview/test/manifest_json.json^headers^ new file mode 100644 index 0000000000..2bab061d43 --- /dev/null +++ b/devtools/client/jsonview/test/manifest_json.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/manifest+json; charset=utf-8 diff --git a/devtools/client/jsonview/test/passthrough-sw.js b/devtools/client/jsonview/test/passthrough-sw.js new file mode 100644 index 0000000000..019d218bc2 --- /dev/null +++ b/devtools/client/jsonview/test/passthrough-sw.js @@ -0,0 +1,5 @@ +"use strict"; + +addEventListener("fetch", evt => { + evt.respondWith(fetch(evt.request)); +}); diff --git a/devtools/client/jsonview/test/simple_json.json b/devtools/client/jsonview/test/simple_json.json new file mode 100644 index 0000000000..d1544242f5 --- /dev/null +++ b/devtools/client/jsonview/test/simple_json.json @@ -0,0 +1 @@ +{ "name": "value" } diff --git a/devtools/client/jsonview/test/simple_json.json^headers^ b/devtools/client/jsonview/test/simple_json.json^headers^ new file mode 100644 index 0000000000..6010bfd188 --- /dev/null +++ b/devtools/client/jsonview/test/simple_json.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json; charset=utf-8 diff --git a/devtools/client/jsonview/test/valid_json.json b/devtools/client/jsonview/test/valid_json.json new file mode 100644 index 0000000000..ca7356ccdb --- /dev/null +++ b/devtools/client/jsonview/test/valid_json.json @@ -0,0 +1,6 @@ +{ + "family": { + "father": "John Doe", + "mother": "Alice Doe" + } +} diff --git a/devtools/client/jsonview/test/valid_json.json^headers^ b/devtools/client/jsonview/test/valid_json.json^headers^ new file mode 100644 index 0000000000..6010bfd188 --- /dev/null +++ b/devtools/client/jsonview/test/valid_json.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json; charset=utf-8 |