summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorBertrand Yuan <noreply@bertyuan.com>2026-03-26 00:02:16 +0800
committerBertrand Yuan <noreply@bertyuan.com>2026-03-26 00:02:16 +0800
commit8a6a6712e7554f110b5ef951f270d88fd010e040 (patch)
tree12cb86b1ede55e15600ef7f139ef7ec91b9fa8a1 /src/lib
parentf7a02fe0e112cf108fc5f22872f1efc077e99fe8 (diff)
add more tests
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/constants.test.ts54
-rw-r--r--src/lib/is-active.test.ts26
-rw-r--r--src/lib/metadata.test.ts58
-rw-r--r--src/lib/safe-action.test.ts42
-rw-r--r--src/lib/utils.test.ts21
-rw-r--r--src/lib/validators/newsletter.test.ts27
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);
+ });
+});
+