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

fix: prevent confetti from triggering on validation errors #195

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
98 changes: 69 additions & 29 deletions packages/untp-playground/__tests__/components/TestResults.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
import { TestResults } from '@/components/TestResults';
import { verifyCredential } from '@/lib/verificationService';
import { TestResults, confettiConfig } from '@/components/TestResults';
import { detectExtension, validateCredentialSchema, validateExtension } from '@/lib/schemaValidation';
import { verifyCredential } from '@/lib/verificationService';
import { Credential } from '@/types/credential';
import { isEnvelopedProof } from '@/lib/credentialService';
import { act, render, screen, waitFor } from '@testing-library/react';
import confetti from 'canvas-confetti';

// Mock the external dependencies
jest.mock('@/lib/verificationService');
Expand All @@ -18,31 +18,15 @@ jest.mock('sonner', () => ({

// Mock sample credentials
const mockBasicCredential = {
original: {
proof: {
type: 'Ed25519Signature2020',
DigitalProductPassport: {
original: {
proof: { type: 'Ed25519Signature2020' },
},
decoded: {
'@context': ['https://vocabulary.uncefact.org/2.0.0/context.jsonld'],
type: ['VerifiableCredential', 'DigitalProductPassport'],
} as Credential,
},
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', () => {
Expand All @@ -66,13 +50,13 @@ describe('TestResults Component', () => {
expect(screen.getByText('DigitalTraceabilityEvent')).toBeInTheDocument();
});

it('enders credential section with correct type and version', async () => {
it('renders 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(<TestResults credentials={{ DigitalProductPassport: mockBasicCredential }} />);
render(<TestResults credentials={mockBasicCredential} />);

// Check for the credential type heading
const credentialSection = screen.getByRole('heading', {
Expand All @@ -81,4 +65,60 @@ describe('TestResults Component', () => {
});
expect(credentialSection).toBeInTheDocument();
});

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

it('shows confetti when all validations pass', async () => {
(verifyCredential as jest.Mock).mockResolvedValue({ verified: true });
(validateCredentialSchema as jest.Mock).mockResolvedValue({ valid: true });
(detectExtension as jest.Mock).mockReturnValue(undefined);

render(<TestResults credentials={mockBasicCredential} />);

await waitFor(() => {
expect(confetti).toHaveBeenCalledTimes(1);
expect(confetti).toHaveBeenCalledWith(expect.objectContaining(confettiConfig));
});
});

it('does not show confetti when schema validation fails', async () => {
(verifyCredential as jest.Mock).mockResolvedValue({ verified: true });
(validateCredentialSchema as jest.Mock).mockResolvedValue({ valid: false });
(detectExtension as jest.Mock).mockReturnValue(undefined);

await act(async () => {
render(<TestResults credentials={mockBasicCredential} />);
});

expect(confetti).not.toHaveBeenCalled();
});

it('does not show confetti when extension validation fails', async () => {
(verifyCredential as jest.Mock).mockResolvedValue({ verified: true });
(validateCredentialSchema as jest.Mock).mockResolvedValue({ valid: true });
(detectExtension as jest.Mock).mockReturnValue('someExtension');
(validateExtension as jest.Mock).mockResolvedValue({ valid: false });

await act(async () => {
render(<TestResults credentials={mockBasicCredential} />);
});

expect(confetti).not.toHaveBeenCalled();
});

it('does not show confetti when verification fails', async () => {
(verifyCredential as jest.Mock).mockResolvedValue({ verified: false });
(validateCredentialSchema as jest.Mock).mockResolvedValue({ valid: true });
(detectExtension as jest.Mock).mockReturnValue(undefined);

await act(async () => {
render(<TestResults credentials={mockBasicCredential} />);
});

expect(confetti).not.toHaveBeenCalled();
});
});
});
52 changes: 23 additions & 29 deletions packages/untp-playground/src/components/TestResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ interface TestGroupProps {
onToggle: () => void;
}

export const confettiConfig = {
particleCount: 200,
spread: 90,
origin: { y: 0.7 },
};

const TestGroup = ({
credentialType,
version,
Expand Down Expand Up @@ -314,11 +320,14 @@ export function TestResults({
validated: true,
};

let allChecksPass = verificationResult.verified;
const extension = detectExtension(credential.decoded);

// Schema validation
try {
const validationResult = await validateCredentialSchema(credential.decoded);
allChecksPass = allChecksPass && validationResult.valid;

setTestResults((prev) => ({
...prev,
[type as CredentialType]: prev[type as CredentialType]?.map((step) =>
Expand All @@ -331,22 +340,8 @@ export function TestResults({
: step,
),
}));

if (!extension && validationResult.valid) {
if (!validatedCredentialsRef.current[credentialType]?.confettiShown) {
confetti({
particleCount: 200,
spread: 90,
origin: { y: 0.7 },
});

validatedCredentialsRef.current[credentialType] = {
...validatedCredentialsRef.current[credentialType]!,
confettiShown: true,
};
}
}
} catch (error) {
allChecksPass = false;
console.log('Schema validation error:', error);
toast.error('Failed to fetch schema. Please try again.');

Expand Down Expand Up @@ -377,6 +372,8 @@ export function TestResults({
if (extension) {
try {
const extensionValidationResult = await validateExtension(credential.decoded);
allChecksPass = allChecksPass && extensionValidationResult.valid;

setTestResults((prev) => ({
...prev,
[type as CredentialType]: prev[type as CredentialType]?.map((step) =>
Expand All @@ -389,21 +386,8 @@ export function TestResults({
: step,
),
}));
if (extensionValidationResult.valid) {
if (!validatedCredentialsRef.current[credentialType]?.confettiShown) {
confetti({
particleCount: 200,
spread: 90,
origin: { y: 0.7 },
});

validatedCredentialsRef.current[credentialType] = {
...validatedCredentialsRef.current[credentialType]!,
confettiShown: true,
};
}
}
} catch (error) {
allChecksPass = false;
console.log('Extension schema validation error:', error);
toast.error('Failed to fetch extension schema. Please try again.');

Expand All @@ -430,6 +414,16 @@ export function TestResults({
}));
}
}

// Trigger confetti only if all checks pass
if (allChecksPass && !validatedCredentialsRef.current[credentialType]?.confettiShown) {
confetti(confettiConfig);

validatedCredentialsRef.current[credentialType] = {
...validatedCredentialsRef.current[credentialType]!,
confettiShown: true,
};
}
} catch (error) {
console.log('Error processing credential:', error);
}
Expand Down
Loading