From ebd4afb1be161a71265d79257d3310e576c7a0df Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:47:19 +0200 Subject: [PATCH 1/2] test: Add Playground console JSON view tests --- src/lib/tests/PlaygroundJsonView.test.js | 174 +++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/lib/tests/PlaygroundJsonView.test.js diff --git a/src/lib/tests/PlaygroundJsonView.test.js b/src/lib/tests/PlaygroundJsonView.test.js new file mode 100644 index 000000000..14d9e3714 --- /dev/null +++ b/src/lib/tests/PlaygroundJsonView.test.js @@ -0,0 +1,174 @@ +/** + * @jest-environment jsdom + */ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +import React, { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import ReactJson from '@microlink/react-json-view'; + +// The Playground console renders logged objects and arrays with +// @microlink/react-json-view. These tests render it with the same props as the +// console output in Playground.react.js. +global.IS_REACT_ACT_ENVIRONMENT = true; + +let container; +let root; + +beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => { + root.unmount(); + }); + container.remove(); + jest.restoreAllMocks(); +}); + +function renderConsoleValue(src) { + act(() => { + root.render( + { + return false; + }} + /> + ); + }); +} + +// Keys of the rendered object entries, without their quotes +function renderedKeys() { + return Array.from(container.querySelectorAll('.object-key'), key => + key.textContent.replace(/"/g, '') + ); +} + +function renderedValues() { + return Array.from(container.querySelectorAll('.variable-value'), value => value.textContent); +} + +// react-json-view shows the copy button of an entry on hover, but the button is +// always in the DOM +function click(button) { + act(() => { + button.click(); + }); +} + +describe('Playground console JSON view', () => { + it('renders the entries of a logged object', () => { + renderConsoleValue({ className: 'GameScore', score: 1337, cheatMode: false }); + expect(renderedKeys()).toEqual(['className', 'score', 'cheatMode']); + expect(renderedValues()).toEqual(['"GameScore"', '1337', 'false']); + }); + + it('renders the items of a logged array', () => { + renderConsoleValue(['GameScore', 1337]); + expect(renderedValues()).toEqual(['"GameScore"', '1337']); + }); + + it('renders a date value', () => { + // The Playground logs the createdAt and updatedAt of a Parse.Object as Date + // objects. The rendered time depends on the time zone, the year does not. + renderConsoleValue({ createdAt: new Date('2026-06-15T12:00:00.000Z') }); + expect(container.querySelector('.date-value').textContent).toContain('2026'); + }); + + it('hides the root name, data types and object sizes', () => { + renderConsoleValue({ player: { name: 'Sean Plott' } }); + expect(renderedKeys()).toEqual(['player', 'name']); + expect(container.textContent).not.toContain('root'); + expect(container.querySelector('.data-type-label')).toBeNull(); + expect(container.querySelector('.object-size')).toBeNull(); + }); + + it('expands two levels and collapses deeper objects', () => { + renderConsoleValue({ level1: { level2: { level3: 'value' } } }); + expect(renderedKeys()).toEqual(['level1', 'level2']); + expect(container.querySelectorAll('.node-ellipsis')).toHaveLength(1); + expect(container.textContent).not.toContain('level3'); + }); + + it('applies the solarized theme and the console style', () => { + renderConsoleValue({ score: 1337 }); + const view = container.querySelector('.react-json-view'); + // base00 of the solarized base16 theme is #002b36 + expect(view.style.backgroundColor).toBe('rgb(0, 43, 54)'); + expect(view.style.marginLeft).toBe('2px'); + expect(view.style.marginBottom).toBe('1px'); + expect(view.style.fontSize).toBe('12px'); + }); + + it('renders without logging errors or warnings', () => { + // The Playground console captures console.error and console.warn, so + // anything logged while rendering a value would show up in the console + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + renderConsoleValue({ + player: { name: 'Sean Plott', scores: [1337, 42] }, + createdAt: new Date('2026-06-15T12:00:00.000Z'), + cheatMode: false, + }); + expect(consoleError).not.toHaveBeenCalled(); + expect(consoleWarn).not.toHaveBeenCalled(); + }); + + it('logs the error message that the Playground console does not capture', () => { + // The Playground console does not capture the errors that react-json-view + // logs, to prevent an infinite loop. It detects them by their message, see + // isReactJsonError in Playground.react.js. + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + renderConsoleValue('GameScore'); + expect(consoleError).toHaveBeenCalledWith( + 'react-json-view error:', + 'src property must be a valid json object' + ); + }); +}); + +describe('Playground console JSON view clipboard', () => { + let writeText; + + beforeEach(() => { + writeText = jest.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true }); + }); + + afterEach(() => { + delete navigator.clipboard; + }); + + it('copies a logged object as formatted JSON', () => { + renderConsoleValue({ className: 'GameScore', score: 1337 }); + // The copy button of the logged object is in its metadata, after its entries + const rootNode = container.querySelector('.object-key-val'); + click(rootNode.querySelector(':scope > .object-meta-data .copy-to-clipboard-container > span')); + expect(writeText).toHaveBeenCalledTimes(1); + expect(writeText).toHaveBeenCalledWith('{\n "className": "GameScore",\n "score": 1337\n}'); + }); + + it('copies a number value', () => { + renderConsoleValue({ score: 1337 }); + click(container.querySelector('.variable-row .copy-to-clipboard-container > span')); + expect(writeText).toHaveBeenCalledTimes(1); + expect(writeText).toHaveBeenCalledWith('1337'); + }); +}); From 81477e0a0b3f8b25108737eb8064d1910b920aba Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:48:29 +0200 Subject: [PATCH 2/2] refactor: Bump @microlink/react-json-view from 1.31.22 to 1.31.40 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51ce83b0f..b36071ba4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@babel/runtime": "7.29.2", "@babel/runtime-corejs3": "7.29.2", "@graphiql/toolkit": "0.12.1", - "@microlink/react-json-view": "1.31.22", + "@microlink/react-json-view": "1.31.40", "bcryptjs": "3.0.3", "body-parser": "2.3.0", "chart.js": "4.5.1", @@ -4980,9 +4980,9 @@ "license": "MIT" }, "node_modules/@microlink/react-json-view": { - "version": "1.31.22", - "resolved": "https://registry.npmjs.org/@microlink/react-json-view/-/react-json-view-1.31.22.tgz", - "integrity": "sha512-cfszidqClRqjnF5YQ5PSGeORo9MnMZvDv+csgq92G1LRZduWEBjeHXKLnulvhE7bX9+ORvjRCWcMQfwRlhPxmA==", + "version": "1.31.40", + "resolved": "https://registry.npmjs.org/@microlink/react-json-view/-/react-json-view-1.31.40.tgz", + "integrity": "sha512-IqeoQMppY8bL/y2ghr3NH6ARq8wTlhcONBkIH7gY2jkWC7T5Rj5wB+pDAlyMn77TZ8kpiPKsRCOqAvbvc75HGA==", "license": "MIT", "dependencies": { "react-base16-styling": "~0.10.0", diff --git a/package.json b/package.json index 71779db36..584779c80 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@babel/runtime": "7.29.2", "@babel/runtime-corejs3": "7.29.2", "@graphiql/toolkit": "0.12.1", - "@microlink/react-json-view": "1.31.22", + "@microlink/react-json-view": "1.31.40", "bcryptjs": "3.0.3", "body-parser": "2.3.0", "chart.js": "4.5.1",