tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit a0a1d82648223e9659299307eb4d8a438edf501f
parent 8e3e96c53183d48fc2391176e0a9e0efc2347c7c
Author: Florian Quèze <florian@queze.net>
Date:   Fri, 31 Oct 2025 20:40:10 +0000

Bug 1997087 - Make the parsed JSON object accessible from the devtools console as $json.data, r=devtools-reviewers,ochameau,nchevobbe.

Differential Revision: https://phabricator.services.mozilla.com/D270495

Diffstat:
Mdevtools/client/jsonview/json-viewer.mjs | 38++++++++++++++++++++++++++++++++++++++
Mdevtools/client/jsonview/test/browser.toml | 2++
Adevtools/client/jsonview/test/browser_jsonview_access_data.js | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdevtools/docs/user/json_viewer/index.rst | 11+++++++++++
4 files changed, 202 insertions(+), 0 deletions(-)

diff --git a/devtools/client/jsonview/json-viewer.mjs b/devtools/client/jsonview/json-viewer.mjs @@ -194,6 +194,44 @@ const promise = (async function parseJSON() { const jsonString = input.jsonText.textContent; try { input.json = parseJsonLossless(jsonString); + + // Expose a clean public API for accessing JSON data from the console + // This is not tied to internal implementation details + window.$json = { + // The parsed JSON data + get data() { + return input.json; + }, + // The original JSON text + get text() { + return jsonString; + }, + // HTTP headers + get headers() { + return JSONView.headers; + }, + }; + + // Log a welcome message to the console + const intro = "font-size: 130%;"; + const bold = "font-family: monospace; font-weight: bold;"; + const reset = ""; + console.log( + "%cData available from the console:%c\n\n" + + "%c$json.data%c - The parsed JSON object\n" + + "%c$json.text%c - The original JSON text\n" + + "%c$json.headers%c - HTTP request and response headers\n\n" + + "The JSON Viewer is documented here:\n" + + "https://firefox-source-docs.mozilla.org/devtools-user/json_viewer/", + intro, + reset, + bold, + reset, + bold, + reset, + bold, + reset + ); } catch (err) { input.json = err; // Display the raw data tab for invalid json diff --git a/devtools/client/jsonview/test/browser.toml b/devtools/client/jsonview/test/browser.toml @@ -25,6 +25,8 @@ support-files = [ ["browser_json_refresh.js"] +["browser_jsonview_access_data.js"] + ["browser_jsonview_bug_1380828.js"] ["browser_jsonview_chunked_json.js"] diff --git a/devtools/client/jsonview/test/browser_jsonview_access_data.js b/devtools/client/jsonview/test/browser_jsonview_access_data.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that $json.data is accessible from the console and contains + * the parsed JSON object. + */ + +/** + * Helper function to get console messages from the content page. + */ +function getConsoleMessages() { + const consoleStorageInner = Cc["@mozilla.org/consoleAPI-storage;1"]; + const storageInner = consoleStorageInner.getService(Ci.nsIConsoleAPIStorage); + const events = storageInner.getEvents(); + + // Find messages from the JSON viewer + return events + .filter(e => e.arguments && !!e.arguments.length) + .map(e => { + // Get the formatted message text + return e.arguments.map(arg => String(arg)).join(" "); + }); +} + +add_task(async function test_console_welcome_message() { + // Clear any existing console messages + const consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + const storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + storage.clearEvents(); + + await addJsonViewTab("data:application/json," + JSON.stringify({ test: 1 })); + + // Get console messages + const messages = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + getConsoleMessages + ); + + // Check that the welcome message was logged + const welcomeMessage = messages.find(msg => + msg.includes("Data available from the console") + ); + ok(welcomeMessage, "Console welcome message was logged"); + + // Verify the message documents the available fields + ok(welcomeMessage.includes("$json.data"), "Message documents $json.data"); + ok(welcomeMessage.includes("$json.text"), "Message documents $json.text"); + ok( + welcomeMessage.includes("$json.headers"), + "Message documents $json.headers" + ); +}); + +/** + * Load the JSON viewer for the given json string and get $json.data from it. + */ +async function getJSONViewData(json) { + await addJsonViewTab("data:application/json," + json); + + return await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return content.wrappedJSObject.$json?.data; + }); +} + +/** + * Helper function to test that $json.data matches the original object. + * @param {*} obj - The object to stringify, load, and verify + * @param {string} dataDescription - Description of the data being tested + */ +async function testJSONViewData(obj, dataDescription) { + const json = JSON.stringify(obj); + const data = await getJSONViewData(json); + + Assert.deepEqual(data, obj, `$json.data matches ${dataDescription}`); +} + +add_task(async function test_jsonview_data_object() { + await testJSONViewData( + { + name: "test", + values: [1, 2, 3], + nested: { foo: "bar" }, + }, + "original object" + ); +}); + +add_task(async function test_jsonview_data_array() { + await testJSONViewData([10, 20, 30, { key: "value" }], "original array"); +}); + +add_task(async function test_jsonview_data_primitive() { + await testJSONViewData(42, "original number"); +}); + +add_task(async function test_jsonview_data_string() { + await testJSONViewData("hello world", "original string"); +}); + +add_task(async function test_jsonview_data_null() { + await testJSONViewData(null, "original null"); +}); + +add_task(async function test_jsonview_data_invalid_json() { + const invalidJson = "{this is not valid json}"; + const data = await getJSONViewData(invalidJson); + + is(data, undefined, "$json.data is undefined for invalid JSON"); +}); + +add_task(async function test_jsonview_data_large_array() { + const largeArray = Array(1000) + .fill(null) + .map((_, i) => i * 2); + + await testJSONViewData(largeArray, "large array"); +}); + +add_task(async function test_jsonview_text() { + const obj = { name: "test", value: 42 }; + const json = JSON.stringify(obj); + await addJsonViewTab("data:application/json," + json); + + const text = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return content.wrappedJSObject.$json?.text; + }); + + is(text, json, "$json.text matches the original JSON text"); +}); + +add_task(async function test_jsonview_headers() { + await addJsonViewTab( + "data:application/json," + JSON.stringify({ foo: "bar" }) + ); + + const headers = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return content.wrappedJSObject.$json?.headers; + } + ); + + ok(headers, "$json.headers is defined"); + ok(Array.isArray(headers.response), "$json.headers.response is an array"); + ok(Array.isArray(headers.request), "$json.headers.request is an array"); +}); diff --git a/devtools/docs/user/json_viewer/index.rst b/devtools/docs/user/json_viewer/index.rst @@ -9,3 +9,14 @@ The JSON viewer provides a search box that you can use to filter the JSON. You can also view the raw JSON and pretty-print it. Finally, if the document was the result of a network request, the viewer displays the request and response headers. + +Console API +=========== + +The JSON viewer exposes data through a console API, making it easy to access the parsed JSON and other information from the Console: + +- **$json.data** - The parsed JSON object +- **$json.text** - The original JSON text +- **$json.headers** - HTTP request and response headers + +To access these from the console, :ref:`open the Web Console <keyboard-shortcuts-opening-and-closing-tools>`, and type ``$json.data`` to explore the parsed JSON object.