diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/constants.test.ts | 54 | ||||
| -rw-r--r-- | src/lib/is-active.test.ts | 26 | ||||
| -rw-r--r-- | src/lib/metadata.test.ts | 58 | ||||
| -rw-r--r-- | src/lib/safe-action.test.ts | 42 | ||||
| -rw-r--r-- | src/lib/utils.test.ts | 21 | ||||
| -rw-r--r-- | src/lib/validators/newsletter.test.ts | 27 |
6 files changed, 228 insertions, 0 deletions
diff --git a/src/lib/constants.test.ts b/src/lib/constants.test.ts new file mode 100644 index 0000000..65fb466 --- /dev/null +++ b/src/lib/constants.test.ts @@ -0,0 +1,54 @@ +import { afterAll, beforeEach, describe, expect, test, vi } from 'vitest'; + +const originalEnv = process.env; + +function setEnv(key: string, value?: string) { + if (value === undefined) { + delete process.env[key]; + return; + } + + process.env[key] = value; +} + +describe('constants', () => { + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + test('uses localhost base url outside production', async () => { + setEnv('NODE_ENV', 'development'); + setEnv('VERCEL_PROJECT_PRODUCTION_URL', 'example.com'); + + const constants = await import('./constants'); + + expect(constants.isProduction).toBe(false); + expect(constants.baseUrl.href).toBe('http://localhost:3000/'); + }); + + test('uses vercel production url in production', async () => { + setEnv('NODE_ENV', 'production'); + setEnv('VERCEL_PROJECT_PRODUCTION_URL', 'blog.example.com'); + + const constants = await import('./constants'); + + expect(constants.isProduction).toBe(true); + expect(constants.baseUrl.href).toBe('https://blog.example.com/'); + }); + + test('falls back to localhost when production url is missing', async () => { + setEnv('NODE_ENV', 'production'); + setEnv('VERCEL_PROJECT_PRODUCTION_URL'); + + const constants = await import('./constants'); + + expect(constants.isProduction).toBe(true); + expect(constants.baseUrl.href).toBe('http://localhost:3000/'); + }); +}); + diff --git a/src/lib/is-active.test.ts b/src/lib/is-active.test.ts new file mode 100644 index 0000000..fcc42d0 --- /dev/null +++ b/src/lib/is-active.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from 'vitest'; +import { isActive } from './is-active'; + +describe('isActive', () => { + test('returns true for exact match', () => { + expect(isActive('/posts', '/posts')).toBe(true); + }); + + test('normalizes trailing slashes before comparing', () => { + expect(isActive('/posts/', '/posts')).toBe(true); + expect(isActive('/posts', '/posts/')).toBe(true); + }); + + test('returns true for nested paths when nested is enabled', () => { + expect(isActive('/posts', '/posts/hello-world', true)).toBe(true); + }); + + test('returns false for nested paths when nested is disabled', () => { + expect(isActive('/posts', '/posts/hello-world', false)).toBe(false); + }); + + test('returns false for unrelated paths', () => { + expect(isActive('/posts', '/tags')).toBe(false); + }); +}); + diff --git a/src/lib/metadata.test.ts b/src/lib/metadata.test.ts new file mode 100644 index 0000000..ce5c5b0 --- /dev/null +++ b/src/lib/metadata.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from 'vitest'; +import { createMetadata } from './metadata'; + +describe('createMetadata', () => { + test('builds metadata with default open graph, twitter, and alternates fields', () => { + const metadata = createMetadata({ + title: 'My Post', + description: 'Post description', + }); + + expect(metadata.openGraph).toMatchObject({ + title: 'My Post', + description: 'Post description', + url: 'https://blog.techwithanirudh.com', + images: '/banner.png', + siteName: 'Blog', + }); + + expect(metadata.twitter).toMatchObject({ + card: 'summary_large_image', + creator: '@AnirudhWith', + title: 'My Post', + description: 'Post description', + images: '/banner.png', + }); + + expect(metadata.alternates).toMatchObject({ + canonical: '/', + types: { + 'application/rss+xml': '/api/rss.xml', + }, + }); + }); + + test('allows nested metadata fields to override defaults and top-level fallbacks', () => { + const metadata = createMetadata({ + title: 'Top Level Title', + openGraph: { + title: 'OG Title', + url: 'https://example.com/posts/1', + }, + twitter: { + title: 'Twitter Title', + card: 'summary', + }, + alternates: { + canonical: '/posts/1', + }, + }); + + expect(metadata.openGraph?.title).toBe('OG Title'); + expect(metadata.openGraph?.url).toBe('https://example.com/posts/1'); + expect(metadata.twitter?.title).toBe('Twitter Title'); + expect(metadata.twitter?.card).toBe('summary'); + expect(metadata.alternates?.canonical).toBe('/posts/1'); + }); +}); + diff --git a/src/lib/safe-action.test.ts b/src/lib/safe-action.test.ts new file mode 100644 index 0000000..eee5679 --- /dev/null +++ b/src/lib/safe-action.test.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +vi.mock('next-safe-action', () => ({ + DEFAULT_SERVER_ERROR_MESSAGE: 'Internal server error', + createSafeActionClient: vi.fn((options: unknown) => options), +})); + +describe('safe-action', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.resetModules(); + }); + + test('returns custom message for ActionError', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { ActionError, actionClient } = await import('./safe-action'); + const client = actionClient as unknown as { + handleServerError: (e: Error) => string; + }; + + const message = client.handleServerError(new ActionError('Custom error')); + + expect(message).toBe('Custom error'); + expect(errorSpy).toHaveBeenCalledWith( + 'Failed to execute action:', + 'Custom error', + ); + }); + + test('returns default message for unknown errors', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const { actionClient } = await import('./safe-action'); + const client = actionClient as unknown as { + handleServerError: (e: Error) => string; + }; + + const message = client.handleServerError(new Error('Unexpected')); + + expect(message).toBe('Internal server error'); + expect(errorSpy).toHaveBeenCalledWith('Failed to execute action:', 'Unexpected'); + }); +}); diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts new file mode 100644 index 0000000..9a61d41 --- /dev/null +++ b/src/lib/utils.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest'; +import { cn } from './utils'; + +describe('cn', () => { + test('merges class names and resolves tailwind conflicts', () => { + expect(cn('px-2', 'px-4')).toBe('px-4'); + }); + + test('handles conditional and falsy inputs', () => { + expect(cn('text-sm', false && 'hidden', undefined, 'font-medium')).toBe( + 'text-sm font-medium', + ); + }); + + test('supports object and array style class inputs', () => { + expect( + cn(['inline-flex', ['items-center']], { 'cursor-pointer': true }), + ).toBe('inline-flex items-center cursor-pointer'); + }); +}); + diff --git a/src/lib/validators/newsletter.test.ts b/src/lib/validators/newsletter.test.ts new file mode 100644 index 0000000..12e3f46 --- /dev/null +++ b/src/lib/validators/newsletter.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from 'vitest'; +import { NewsletterSchema } from './newsletter'; + +describe('NewsletterSchema', () => { + test('accepts a valid email payload', () => { + const parsed = NewsletterSchema.parse({ + email: 'user@example.com', + }); + + expect(parsed).toEqual({ email: 'user@example.com' }); + }); + + test('rejects invalid email payloads', () => { + expect( + NewsletterSchema.safeParse({ + email: 'invalid-email', + }).success, + ).toBe(false); + + expect( + NewsletterSchema.safeParse({ + email: '', + }).success, + ).toBe(false); + }); +}); + |
