Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: update unit test in untp playground #182

Merged
merged 4 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading