Skip to content

Commit

Permalink
test: update unit test in untp playground (#182)
Browse files Browse the repository at this point in the history
* test: update unit test in untp playground

* test: improve unit test in untp playground

* test: update test in untp playground test result
  • Loading branch information
ldhyen99 authored Dec 23, 2024
1 parent 5f8c113 commit 2f3be64
Show file tree
Hide file tree
Showing 9 changed files with 800 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// packages/untp-playground/src/components/CredentialUploader.test.tsx
import '@testing-library/jest-dom';
import React, { act } from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

import { CredentialUploader } from '@/components/CredentialUploader';
import { toast } from 'sonner';

// Mock the toast library
jest.mock('sonner', () => ({
toast: {
error: jest.fn(),
},
}));

describe('CredentialUploader component', () => {
const mockOnCredentialUpload = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
JSON.parse = JSON.parse;
});

it('renders the CredentialUploader component', () => {
render(<CredentialUploader onCredentialUpload={mockOnCredentialUpload} />);
const dropzoneElement = screen.getByText(/drag and drop credentials here/i);
expect(dropzoneElement).toBeInTheDocument();
});

it('calls onCredentialUpload with valid JSON file', async () => {
const returnValue = {
key: 'value',
};

render(<CredentialUploader onCredentialUpload={mockOnCredentialUpload} />);
const inputElement = screen.getByRole('presentation').querySelector('input[type="file"]');

const validJsonFile = new File([JSON.stringify(returnValue)], 'valid.json', {
type: 'application/json',
});

await act(async () => {
fireEvent.change(inputElement as Element, { target: { files: [validJsonFile] } });
await new Promise((resolve) => setTimeout(resolve, 0));
});

await waitFor(async () => {
expect(mockOnCredentialUpload).toHaveBeenCalledWith(returnValue);
});
});

it('displays an error for invalid JSON content', async () => {
// JSON.parse = jest.fn().mockRejectedValue(new Error('Invalid JSON'));
render(<CredentialUploader onCredentialUpload={mockOnCredentialUpload} />);
const inputElement = screen.getByRole('presentation').querySelector('input[type="file"]');

const invalidJsonFile = new File(['invalid json'], 'invalid.json', {
type: 'application/json',
});

await act(async () => {
fireEvent.change(inputElement as Element, { target: { files: [invalidJsonFile] } });
await new Promise((resolve) => setTimeout(resolve, 0));
});

expect(toast.error).toHaveBeenCalledWith('Invalid format - File must contain valid JSON');
});

it('displays an error for invalid file types', async () => {
render(<CredentialUploader onCredentialUpload={mockOnCredentialUpload} />);
// Find the input element of type file
const inputElement = screen.getByRole('presentation').querySelector('input[type="file"]');
const invalidFile = new File(['content'], 'invalid.pdf', { type: 'application/pdf' });

await act(async () => {
fireEvent.change(inputElement as Element, { target: { files: [invalidFile] } });
await new Promise((resolve) => setTimeout(resolve, 0));
});
expect(toast.error).toHaveBeenCalledWith('Invalid file format. Please upload only .json, .jwt, or .txt files.');
});

it('displays an error for invalid JWT content', async () => {
render(<CredentialUploader onCredentialUpload={mockOnCredentialUpload} />);
const inputElement = screen.getByRole('presentation').querySelector('input[type="file"]');

const invalidJwtFile = new File(['invalid jwt'], 'invalid.jwt', {
type: 'text/plain',
});

await act(async () => {
fireEvent.change(inputElement as Element, { target: { files: [invalidJwtFile] } });
// await new Promise((resolve) => setTimeout(resolve, 0));
});

await waitFor(async () => {
expect(toast.error).toHaveBeenCalledWith(
'Invalid JWT format - Please provide a file containing a valid JWT token',
);
});
});

it('shows generic error when credential processing fails', async () => {
// Mock onCredentialUpload to throw an error
const mockOnCredentialUpload = jest.fn().mockImplementation(() => {
throw new Error('Some unexpected error');
});
render(<CredentialUploader onCredentialUpload={mockOnCredentialUpload} />);

const inputElement = screen.getByRole('presentation').querySelector('input[type="file"]');

const validJSON = JSON.stringify({ key: 'value' });
const file = new File([validJSON], 'test.json', { type: 'application/json' });

await act(async () => {
fireEvent.change(inputElement as Element, { target: { files: [file] } });
await new Promise((resolve) => setTimeout(resolve, 0));
});

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(
'Failed to process credential - Please ensure the file contains valid data',
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { act } from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { DownloadCredential } from '@/components/DownloadCredential';

// Mock the fetch function
global.fetch = jest.fn();
// Mock URL.createObjectURL and URL.revokeObjectURL
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();

describe('DownloadCredential', () => {
// Reset all mocks before each test
beforeEach(() => {
jest.clearAllMocks();
// Mock successful response
(global.fetch as jest.Mock).mockResolvedValue({
json: () => Promise.resolve({ test: 'data' }),
});
(global.URL.createObjectURL as jest.Mock).mockReturnValue('blob:test-url');
});

it('renders download button with correct text and icon', () => {
render(<DownloadCredential />);

const button = screen.getByRole('button', { name: /download test credential/i });
expect(button).toBeInTheDocument();
expect(button.querySelector('svg')).toBeInTheDocument();
});

it('handles download click successfully', async () => {
render(<DownloadCredential />);

await act(async () => {
const button = screen.getByRole('button');
await fireEvent.click(button);
});

await waitFor(() => {
// Check if fetch was called with correct path
expect(fetch).toHaveBeenCalledWith('/credentials/dpp.json');

// Verify Blob creation
expect(window.URL.createObjectURL).toHaveBeenCalled();
const blobCall = (window.URL.createObjectURL as jest.Mock).mock.calls[0][0];
expect(blobCall instanceof Blob).toBeTruthy();
expect(blobCall.type).toBe('application/json');

// Verify cleanup
expect(window.URL.revokeObjectURL).toHaveBeenCalledWith('blob:test-url');
});
});

it('handles download error gracefully', async () => {
// Mock console.log to verify error logging
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();

// Mock fetch to reject
(global.fetch as jest.Mock).mockRejectedValue(new Error('Download failed'));

render(<DownloadCredential />);

const button = screen.getByRole('button');
await fireEvent.click(button);

expect(consoleSpy).toHaveBeenCalledWith('Error downloading credential:', expect.any(Error));
});
});
170 changes: 170 additions & 0 deletions packages/untp-playground/__tests__/components/ErrorDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* @jest-environment jsdom
*/

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ErrorDialog } from '@/components/ErrorDialog';

// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(),
},
});

describe('ErrorDialog', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('returns null when no errors are provided', () => {
const { container } = render(<ErrorDialog errors={[]} />);
expect(container.firstChild).toBeNull();
});

it('returns null when errors is not an array', () => {
// @ts-ignore - Testing invalid input
const { container } = render(<ErrorDialog errors={{}} />);
expect(container.firstChild).toBeNull();
});

it('displays validation errors correctly', () => {
const errors = [
{
keyword: 'type',
instancePath: '/data/field1',
params: { type: 'string' },
},
{
keyword: 'required',
instancePath: '/data',
params: { missingProperty: 'requiredField' },
},
] as any;

render(<ErrorDialog errors={errors} />);

// Check if error count is displayed
expect(screen.getByText(/we found 2 issues/i)).toBeInTheDocument();

// Check if error locations are displayed
expect(screen.getByText(/data → field1/i)).toBeInTheDocument();
expect(screen.getByText(/wrong type/i)).toBeInTheDocument();
expect(screen.getByText(/missing field/i)).toBeInTheDocument();
});

it('displays warnings for additional properties', () => {
const errors = [
{
keyword: 'additionalProperties',
instancePath: '',
params: { additionalProperty: 'extraField' },
},
] as any;

render(<ErrorDialog errors={errors} />);

expect(screen.getByText(/1 warning/i)).toBeInTheDocument();
expect(screen.getByText(/additional property: "extraField"/i)).toBeInTheDocument();
});

it('handles expandable error details', async () => {
const errors = [
{
keyword: 'enum',
instancePath: '/data/status',
params: { allowedValues: ['active', 'inactive'] },
},
] as any;

render(<ErrorDialog errors={errors} />);

// Click to expand error details
const button = screen.getByRole('button', { name: /choose from allowed values/i });
fireEvent.click(button);

// Check if expanded content is visible
expect(screen.getByText(/must be one of:/i)).toBeInTheDocument();
expect(screen.getByText(/active, inactive/i)).toBeInTheDocument();
});

it('handles copy functionality', async () => {
const errors = [
{
keyword: 'const',
instancePath: '/data/type',
params: { allowedValue: 'user' },
},
] as any;

render(<ErrorDialog errors={errors} />);

// Expand the error details
const expandButton = screen.getByRole('button', { name: /use the correct value/i });
fireEvent.click(expandButton);

// Click copy button
const copyButton = screen.getByRole('button', { name: /copy/i });
fireEvent.click(copyButton);

// Verify clipboard API was called
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('"user"');

// Verify "Copied!" text appears
expect(screen.getByText(/copied!/i)).toBeInTheDocument();
});

it('displays correct tips based on error type', () => {
const errors = [
{
keyword: 'const',
instancePath: '/data/type',
params: { allowedValue: 'user' },
},
] as any;

render(<ErrorDialog errors={errors} />);

// Expand the error details
const expandButton = screen.getByRole('button', { name: /use the correct value/i });
fireEvent.click(expandButton);

// Verify tip content
expect(screen.getByText(/this value must match exactly as shown above/i)).toBeInTheDocument();
});

it('groups multiple errors for the same path', () => {
const errors = [
{
keyword: 'type',
instancePath: '/data/field1',
params: { type: 'string' },
},
{
keyword: 'minLength',
instancePath: '/data/field1',
params: { limit: 3 },
},
] as any;

render(<ErrorDialog errors={errors} />);

// Should show only one group for '/data/field1'
expect(screen.getByText(/we found 1 issue/i)).toBeInTheDocument();
expect(screen.getByText(/data → field1/i)).toBeInTheDocument();
});

it('applies custom className when provided', () => {
const errors = [
{
keyword: 'type',
instancePath: '/data/field1',
params: { type: 'string' },
},
] as any;

const { container } = render(<ErrorDialog errors={errors} className='custom-class' />);
expect(container.firstChild).toHaveClass('custom-class');
});
});
Loading

0 comments on commit 2f3be64

Please sign in to comment.