diff --git a/README.md b/README.md index 6c61ee19..da72de4e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![npm](https://img.shields.io/npm/v/hyperparam)](https://www.npmjs.com/package/hyperparam) [![workflow status](https://github.com/hyparam/hyperparam-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/hyparam/hyperparam-cli/actions) [![mit license](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -![coverage](https://img.shields.io/badge/Coverage-59-darkred) +![coverage](https://img.shields.io/badge/Coverage-62-darkred) 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', + })) + }) + }) +})