diff --git a/packages/untp-playground/__tests__/components/CredentialUploader.test.tsx b/packages/untp-playground/__tests__/components/CredentialUploader.test.tsx new file mode 100644 index 00000000..02d836ec --- /dev/null +++ b/packages/untp-playground/__tests__/components/CredentialUploader.test.tsx @@ -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(); + 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(); + 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(); + 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(); + // 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(); + 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(); + + 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', + ); + }); + }); +}); diff --git a/packages/untp-playground/__tests__/components/DownloadCredential.test.tsx b/packages/untp-playground/__tests__/components/DownloadCredential.test.tsx new file mode 100644 index 00000000..140e7ec9 --- /dev/null +++ b/packages/untp-playground/__tests__/components/DownloadCredential.test.tsx @@ -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(); + + 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(); + + 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(); + + const button = screen.getByRole('button'); + await fireEvent.click(button); + + expect(consoleSpy).toHaveBeenCalledWith('Error downloading credential:', expect.any(Error)); + }); +}); diff --git a/packages/untp-playground/__tests__/components/ErrorDialog.test.tsx b/packages/untp-playground/__tests__/components/ErrorDialog.test.tsx new file mode 100644 index 00000000..27a40fe3 --- /dev/null +++ b/packages/untp-playground/__tests__/components/ErrorDialog.test.tsx @@ -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(); + expect(container.firstChild).toBeNull(); + }); + + it('returns null when errors is not an array', () => { + // @ts-ignore - Testing invalid input + const { container } = render(); + 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(); + + // 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(); + + 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(); + + // 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(); + + // 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(); + + // 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(); + + // 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(); + expect(container.firstChild).toHaveClass('custom-class'); + }); +}); diff --git a/packages/untp-playground/__tests__/components/Footer.test.tsx b/packages/untp-playground/__tests__/components/Footer.test.tsx new file mode 100644 index 00000000..a530182b --- /dev/null +++ b/packages/untp-playground/__tests__/components/Footer.test.tsx @@ -0,0 +1,70 @@ +/** + * @jest-environment jsdom + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Footer } from '@/components/Footer'; + +describe('Footer', () => { + it('renders correctly', () => { + render(