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();
+
+ // Test container classes
+ const footer = screen.getByRole('contentinfo');
+ expect(footer).toHaveClass('border-t');
+
+ const container = footer.firstElementChild;
+ expect(container).toHaveClass('container', 'mx-auto', 'p-8', 'max-w-7xl');
+ });
+
+ it('renders all links with correct attributes', () => {
+ render();
+
+ // Test UNTP Specification link
+ const specLink = screen.getByRole('link', { name: /untp specification/i });
+ expect(specLink).toHaveAttribute('href', 'https://uncefact.github.io/spec-untp/');
+ expect(specLink).toHaveAttribute('target', '_blank');
+ expect(specLink).toHaveAttribute('rel', 'noopener noreferrer');
+ expect(specLink).toHaveClass('hover:text-foreground', 'transition-colors');
+
+ // Test UNTP Test Suite link
+ const testSuiteLink = screen.getByRole('link', { name: /untp test suite/i });
+ expect(testSuiteLink).toHaveAttribute('href', 'https://uncefact.github.io/tests-untp/');
+ expect(testSuiteLink).toHaveAttribute('target', '_blank');
+ expect(testSuiteLink).toHaveAttribute('rel', 'noopener noreferrer');
+ expect(testSuiteLink).toHaveClass('hover:text-foreground', 'transition-colors');
+ });
+
+ it('renders separator dot with correct visibility classes', () => {
+ render();
+
+ const separator = screen.getByText('•');
+ expect(separator).toHaveClass('hidden', 'md:inline');
+ });
+
+ it('has correct responsive layout classes', () => {
+ render();
+
+ const linksContainer = screen.getByText('•').parentElement;
+ expect(linksContainer).toHaveClass(
+ 'flex',
+ 'flex-col',
+ 'md:flex-row',
+ 'justify-center',
+ 'items-center',
+ 'gap-4',
+ 'text-sm',
+ 'text-muted-foreground',
+ );
+ });
+
+ it('renders links in correct order', () => {
+ render();
+
+ const links = screen.getAllByRole('link');
+ expect(links).toHaveLength(2);
+ expect(links[0]).toHaveTextContent('UNTP Specification');
+ expect(links[1]).toHaveTextContent('UNTP Test Suite');
+ });
+});
diff --git a/packages/untp-playground/__tests__/components/Header.test.tsx b/packages/untp-playground/__tests__/components/Header.test.tsx
new file mode 100644
index 00000000..58d546aa
--- /dev/null
+++ b/packages/untp-playground/__tests__/components/Header.test.tsx
@@ -0,0 +1,40 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { Header } from '@/components/Header';
+
+describe('Header', () => {
+ it('renders correctly', () => {
+ render();
+
+ // Test container element
+ const header = screen.getByRole('banner');
+ expect(header).toHaveClass('border-b');
+ });
+
+ it('has correct container classes', () => {
+ render();
+
+ const container = screen.getByRole('banner').firstElementChild;
+ expect(container).toHaveClass('container', 'mx-auto', 'p-8', 'max-w-7xl', 'flex', 'items-center', 'gap-4');
+ });
+
+ it('renders the title correctly', () => {
+ render();
+
+ const heading = screen.getByRole('heading', { level: 1 });
+ expect(heading).toHaveTextContent('UNTP Playground');
+ expect(heading).toHaveClass('text-2xl', 'font-bold');
+ });
+
+ // If we uncomment the Image component in the future
+ // it('renders the logo image when enabled', () => {
+ // render();
+ //
+ // const logo = screen.getByAltText('UNTP Logo');
+ // expect(logo).toBeInTheDocument();
+ //
+});
diff --git a/packages/untp-playground/__tests__/components/TestResults.test.tsx b/packages/untp-playground/__tests__/components/TestResults.test.tsx
new file mode 100644
index 00000000..ad3dbc66
--- /dev/null
+++ b/packages/untp-playground/__tests__/components/TestResults.test.tsx
@@ -0,0 +1,84 @@
+import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
+import { TestResults } from '@/components/TestResults';
+import { verifyCredential } from '@/lib/verificationService';
+import { detectExtension, validateCredentialSchema, validateExtension } from '@/lib/schemaValidation';
+import { Credential } from '@/types/credential';
+import { isEnvelopedProof } from '@/lib/credentialService';
+
+// Mock the external dependencies
+jest.mock('@/lib/verificationService');
+jest.mock('@/lib/schemaValidation');
+jest.mock('@/lib/credentialService');
+jest.mock('canvas-confetti');
+jest.mock('sonner', () => ({
+ toast: {
+ error: jest.fn(),
+ },
+}));
+
+// Mock sample credentials
+const mockBasicCredential = {
+ original: {
+ proof: {
+ type: 'Ed25519Signature2020',
+ },
+ },
+ decoded: {
+ '@context': ['https://vocabulary.uncefact.org/2.0.0/context.jsonld'],
+ type: ['VerifiableCredential', 'DigitalProductPassport'],
+ } as Credential,
+};
+
+const mockExtensionCredential = {
+ original: {
+ proof: {
+ type: 'Ed25519Signature2020',
+ },
+ },
+ decoded: {
+ '@context': ['https://vocabulary.uncefact.org/2.0.0/context.jsonld'],
+ type: ['VerifiableCredential', 'DigitalProductPassport'],
+ credentialSubject: {
+ type: 'ExtensionType',
+ version: '1.0.0',
+ },
+ } as Credential,
+};
+
+describe('TestResults Component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.resetAllMocks();
+ // Setup default mock implementations
+ (verifyCredential as jest.Mock).mockResolvedValue({ verified: true });
+ (validateCredentialSchema as jest.Mock).mockResolvedValue({ valid: true });
+ (validateExtension as jest.Mock).mockResolvedValue({ valid: true });
+ });
+
+ it('renders empty state correctly', () => {
+ render();
+
+ // Check if all credential types are rendered
+ expect(screen.getByText('DigitalProductPassport')).toBeInTheDocument();
+ expect(screen.getByText('DigitalConformityCredential')).toBeInTheDocument();
+ expect(screen.getByText('DigitalFacilityRecord')).toBeInTheDocument();
+ expect(screen.getByText('DigitalIdentityAnchor')).toBeInTheDocument();
+ expect(screen.getByText('DigitalTraceabilityEvent')).toBeInTheDocument();
+ });
+
+ it('enders credential section with correct type and version', async () => {
+ (detectExtension as jest.Mock).mockReturnValue({
+ core: { type: 'DigitalProductPassport', version: '2.0.0' },
+ extension: { type: 'DigitalProductPassport', version: '2.0.0' },
+ });
+
+ render();
+
+ // Check for the credential type heading
+ const credentialSection = screen.getByRole('heading', {
+ level: 3,
+ name: /DigitalProductPassport.*v2\.0\.0/,
+ });
+ expect(credentialSection).toBeInTheDocument();
+ });
+});
diff --git a/packages/untp-playground/__tests__/lib/formatValidationErrors.test.ts b/packages/untp-playground/__tests__/lib/formatValidationErrors.test.ts
new file mode 100644
index 00000000..3a93cf55
--- /dev/null
+++ b/packages/untp-playground/__tests__/lib/formatValidationErrors.test.ts
@@ -0,0 +1,152 @@
+import { formatValidationError } from '@/lib/formatValidationErrors';
+
+describe('formatValidationError', () => {
+ // Required field tests
+ test('formats root level required field error', () => {
+ const error = {
+ keyword: 'required',
+ instancePath: '',
+ params: { missingProperty: 'name' },
+ };
+ expect(formatValidationError(error)).toBe('Missing required field: name');
+ });
+
+ test('formats nested required field error', () => {
+ const error = {
+ keyword: 'required',
+ instancePath: '/user/profile',
+ params: { missingProperty: 'email' },
+ };
+ expect(formatValidationError(error)).toBe('Missing required field: user → profile → email');
+ });
+
+ // Const value tests
+ test('formats const error with single value', () => {
+ const error = {
+ keyword: 'const',
+ instancePath: '/type',
+ params: { allowedValue: 'user' },
+ };
+ expect(formatValidationError(error)).toBe('Invalid value for type: must be one of [user]');
+ });
+
+ test('formats const error with multiple values', () => {
+ const error = {
+ keyword: 'const',
+ instancePath: '/status',
+ params: { allowedValue: ['active', 'inactive'] },
+ };
+ expect(formatValidationError(error)).toBe('Invalid value for status: must be one of [active or inactive]');
+ });
+
+ // Enum tests
+ test('formats enum error', () => {
+ const error = {
+ keyword: 'enum',
+ instancePath: '/role',
+ params: { allowedValues: ['admin', 'user', 'guest'] },
+ };
+ expect(formatValidationError(error)).toBe('Invalid value for role: must be one of [admin, user, guest]');
+ });
+
+ // Type tests
+ test('formats type error', () => {
+ const error = {
+ keyword: 'type',
+ instancePath: '/age',
+ params: { type: 'number' },
+ };
+ expect(formatValidationError(error)).toBe('Invalid type for age: expected number');
+ });
+
+ // Format tests
+ test('formats format error', () => {
+ const error = {
+ keyword: 'format',
+ instancePath: '/email',
+ params: { format: 'email' },
+ };
+ expect(formatValidationError(error)).toBe('Invalid format for email: must be a valid email');
+ });
+
+ // Pattern tests
+ test('formats pattern error', () => {
+ const error = {
+ keyword: 'pattern',
+ instancePath: '/username',
+ params: { pattern: '^[a-zA-Z0-9]+$' },
+ };
+ expect(formatValidationError(error)).toBe('Invalid format for username: must match pattern ^[a-zA-Z0-9]+$');
+ });
+
+ // Additional properties tests
+ test('formats additional properties error', () => {
+ const error = {
+ keyword: 'additionalProperties',
+ instancePath: '',
+ params: { additionalProperty: 'unknownField' },
+ };
+ expect(formatValidationError(error)).toBe('Unknown field: unknownField');
+ });
+
+ // Default case test
+ test('handles unknown validation error with message', () => {
+ const error = {
+ keyword: 'unknown',
+ instancePath: '',
+ message: 'Custom error message',
+ params: {},
+ };
+ expect(formatValidationError(error)).toBe('Custom error message');
+ });
+
+ test('handles unknown validation error without message', () => {
+ const error = {
+ keyword: 'unknown',
+ instancePath: '',
+ params: {},
+ };
+ expect(formatValidationError(error)).toBe('Unknown validation error');
+ });
+
+ test('formats const error with empty path', () => {
+ const error = {
+ keyword: 'const',
+ instancePath: '',
+ params: { allowedValue: 'root' },
+ };
+ expect(formatValidationError(error)).toBe('Invalid value for field: must be one of [root]');
+ });
+
+ // Test for empty path with enum validation
+ test('formats enum error with empty path', () => {
+ const error = {
+ keyword: 'enum',
+ instancePath: '',
+ params: { allowedValues: ['root1', 'root2'] },
+ };
+ expect(formatValidationError(error)).toBe('Invalid value for field: must be one of [root1, root2]');
+ });
+
+ // Test for path with @ character
+ test('formats error with @ in path', () => {
+ const error = {
+ keyword: 'type',
+ instancePath: '/user/@personal/email',
+ params: { type: 'string' },
+ };
+ expect(formatValidationError(error)).toBe('Invalid type for user → personal → email: expected string');
+ });
+
+ // Test for complex nested path
+ test('formats error with complex nested path', () => {
+ const error = {
+ keyword: 'const',
+ instancePath: '/users/0/@details/settings/theme',
+ params: { allowedValue: 'dark' },
+ };
+ expect(formatValidationError(error)).toBe(
+ 'Invalid value for users → 0 → details → settings → theme: must be one of [dark]',
+ );
+ });
+});
diff --git a/packages/untp-playground/__tests__/lib/verificationService.test.ts b/packages/untp-playground/__tests__/lib/verificationService.test.ts
new file mode 100644
index 00000000..429f5972
--- /dev/null
+++ b/packages/untp-playground/__tests__/lib/verificationService.test.ts
@@ -0,0 +1,84 @@
+import { verifyCredential } from '@/lib/verificationService';
+
+// Mock fetch globally
+global.fetch = jest.fn();
+
+describe('verificationService', () => {
+ // Reset mocks before each test
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ test('successfully verifies a credential', async () => {
+ const mockResponse = { verified: true, results: [] };
+ const mockCredential = { id: '123', type: ['VerifiableCredential'] };
+
+ // Mock successful fetch response
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockResponse,
+ });
+
+ const result = await verifyCredential(mockCredential);
+
+ // Verify the result
+ expect(result).toEqual(mockResponse);
+
+ // Verify fetch was called with correct parameters
+ expect(global.fetch).toHaveBeenCalledWith('https://vckit.untp.showthething.com/agent/routeVerificationCredential', {
+ method: 'POST',
+ headers: {
+ Authorization: 'Bearer test123',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ credential: mockCredential,
+ fetchRemoteContexts: true,
+ policies: {
+ credentialStatus: false,
+ },
+ }),
+ });
+ });
+
+ test('handles non-ok response from API', async () => {
+ const mockCredential = { id: '123', type: ['VerifiableCredential'] };
+
+ // Mock failed fetch response
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 400,
+ statusText: 'Bad Request',
+ });
+
+ // Verify that the function throws an error
+ await expect(verifyCredential(mockCredential)).rejects.toThrow('Verification failed');
+ });
+
+ test('handles network error', async () => {
+ const mockCredential = { id: '123', type: ['VerifiableCredential'] };
+ const networkError = new Error('Network error');
+
+ // Mock network error
+ (global.fetch as jest.Mock).mockRejectedValueOnce(networkError);
+
+ // Verify that the function throws the network error
+ await expect(verifyCredential(mockCredential)).rejects.toThrow(networkError);
+ });
+
+ test('handles JSON parsing error', async () => {
+ const mockCredential = { id: '123', type: ['VerifiableCredential'] };
+ const jsonError = new Error('Invalid JSON');
+
+ // Mock successful fetch but failed JSON parsing
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: async () => {
+ throw jsonError;
+ },
+ });
+
+ // Verify that the function throws the JSON parsing error
+ await expect(verifyCredential(mockCredential)).rejects.toThrow(jsonError);
+ });
+});
diff --git a/packages/untp-playground/jest.config.js b/packages/untp-playground/jest.config.js
index ade2dc81..49e33624 100644
--- a/packages/untp-playground/jest.config.js
+++ b/packages/untp-playground/jest.config.js
@@ -21,7 +21,13 @@ const config = {
testMatch: ['**/__tests__/**/*.(spec|test).[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
testPathIgnorePatterns: ['/node_modules/', '__tests__/mocks/*.ts'],
modulePathIgnorePatterns: ['/build', '/dist', '/.next'],
- collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/index', '!src/**/types'],
+ collectCoverageFrom: [
+ 'src/**/*.{ts,tsx}',
+ '!src/**/*.d.ts',
+ '!src/**/index',
+ '!src/**/types',
+ '!src/components/ui/**',
+ ],
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async