diff --git a/README.md b/README.md
index 6c61ee19..da72de4e 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://www.npmjs.com/package/hyperparam)
[](https://github.com/hyparam/hyperparam-cli/actions)
[](https://opensource.org/licenses/MIT)
-
+
This is the hyperparam cli tool.
diff --git a/package.json b/package.json
index 8e2e6987..66f44d3c 100644
--- a/package.json
+++ b/package.json
@@ -54,13 +54,14 @@
"react-dom": "18.3.1"
},
"devDependencies": {
+ "@eslint/js": "9.22.0",
"@testing-library/react": "16.2.0",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@vitejs/plugin-react": "4.3.4",
"@vitest/coverage-v8": "3.0.8",
- "eslint": "9.21.0",
+ "eslint": "9.22.0",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.19",
diff --git a/src/styles/app.css b/src/styles/app.css
index b4bb0ba7..d341ac4d 100644
--- a/src/styles/app.css
+++ b/src/styles/app.css
@@ -525,6 +525,7 @@ button.close-button:hover {
height: 24px;
margin-right: auto;
outline: none;
+ padding: 0;
transition: color 0.3s;
}
.slideClose::before {
diff --git a/test/components/viewers/JsonView.test.tsx b/test/components/viewers/JsonView.test.tsx
new file mode 100644
index 00000000..695cb811
--- /dev/null
+++ b/test/components/viewers/JsonView.test.tsx
@@ -0,0 +1,117 @@
+import { render, waitFor } from '@testing-library/react'
+import React from 'react'
+import { describe, expect, it, vi } from 'vitest'
+import JsonView from '../../../src/components/viewers/JsonView'
+import { FileSource } from '../../../src/lib/sources/types.js'
+
+vi.mock('../../../src/lib/utils.js', async () => {
+ const actual = await vi.importActual('../../../src/lib/utils.js')
+ return { ...actual, asyncBufferFrom: vi.fn() }
+})
+
+globalThis.fetch = vi.fn()
+
+describe('JsonView Component', () => {
+ const encoder = new TextEncoder()
+
+ it('renders json content as nested lists', async () => {
+ const body = encoder.encode('{"key":"value"}').buffer as ArrayBuffer
+ const source: FileSource = {
+ resolveUrl: 'testKey0',
+ kind: 'file',
+ fileName: 'testKey0',
+ sourceId: 'testKey0',
+ sourceParts: [],
+ }
+ vi.mocked(fetch).mockResolvedValueOnce({
+ status: 200,
+ headers: new Headers({ 'Content-Length': body.byteLength.toString() }),
+ text: () => Promise.resolve('{"key":"value"}'),
+ } as Response)
+
+ const { findByRole, findByText } = render(
+
+ )
+
+ expect(fetch).toHaveBeenCalledWith('testKey0', undefined)
+ // Wait for asynchronous JSON loading and parsing
+ await expect(findByRole('list')).resolves.toBeDefined()
+ await expect(findByText('key:')).resolves.toBeDefined()
+ await expect(findByText('"value"')).resolves.toBeDefined()
+ })
+
+ it('displays an error when the json content is too long', async () => {
+ const source: FileSource = {
+ sourceId: 'testKey1',
+ sourceParts: [],
+ kind: 'file',
+ fileName: 'testKey1',
+ resolveUrl: 'testKey1',
+ }
+ vi.mocked(fetch).mockResolvedValueOnce({
+ status: 200,
+ headers: new Headers({ 'Content-Length': '8000001' }),
+ text: () => Promise.resolve(''),
+ } as Response)
+
+ const setError = vi.fn()
+ render()
+
+ expect(fetch).toHaveBeenCalledWith('testKey1', undefined)
+ await waitFor(() => {
+ expect(setError).toHaveBeenCalledWith(expect.objectContaining({
+ message: 'File is too large to display',
+ }))
+ })
+ })
+
+ it('displays an error when the json content is invalid', async () => {
+ const body = encoder.encode('INVALIDJSON').buffer as ArrayBuffer
+ const source: FileSource = {
+ resolveUrl: 'testKey2',
+ kind: 'file',
+ fileName: 'testKey2',
+ sourceId: 'testKey2',
+ sourceParts: [],
+ }
+ vi.mocked(fetch).mockResolvedValueOnce({
+ status: 200,
+ headers: new Headers({ 'Content-Length': body.byteLength.toString() }),
+ text: () => Promise.resolve('INVALIDJSON'),
+ } as Response)
+
+ const setError = vi.fn()
+ render()
+
+ expect(fetch).toHaveBeenCalledWith('testKey2', undefined)
+ await waitFor(() => {
+ expect(setError).toHaveBeenCalledWith(expect.objectContaining({
+ message: expect.stringContaining('Unexpected token') as string,
+ }))
+ })
+ })
+
+ it('displays an error when unauthorized', async () => {
+ const source: FileSource = {
+ resolveUrl: 'testKey3',
+ kind: 'file',
+ fileName: 'testKey3',
+ sourceId: 'testKey3',
+ sourceParts: [],
+ }
+ vi.mocked(fetch).mockResolvedValueOnce({
+ status: 401,
+ text: () => Promise.resolve('Unauthorized'),
+ } as Response)
+
+ const setError = vi.fn()
+ render()
+
+ expect(fetch).toHaveBeenCalledWith('testKey3', undefined)
+ await waitFor(() => {
+ expect(setError).toHaveBeenCalledWith(expect.objectContaining({
+ message: 'Unauthorized',
+ }))
+ })
+ })
+})