diff --git a/src/routes/(protected)/admin/proclamations/ProclamationForm.svelte b/src/routes/(protected)/admin/proclamations/ProclamationForm.svelte index d996652..d95421a 100644 --- a/src/routes/(protected)/admin/proclamations/ProclamationForm.svelte +++ b/src/routes/(protected)/admin/proclamations/ProclamationForm.svelte @@ -108,14 +108,17 @@ {#if fieldErrors.solfeggioFrequencyId} @@ -133,7 +136,6 @@ {#if fieldErrors.parentCategoryId} @@ -159,14 +164,17 @@ {#if fieldErrors.categoryId} @@ -184,14 +192,17 @@ {#if fieldErrors.subscriptionLevelId} diff --git a/src/routes/(protected)/admin/proclamations/ProclamationForm.svelte.spec.ts b/src/routes/(protected)/admin/proclamations/ProclamationForm.svelte.spec.ts new file mode 100644 index 0000000..7f64781 --- /dev/null +++ b/src/routes/(protected)/admin/proclamations/ProclamationForm.svelte.spec.ts @@ -0,0 +1,164 @@ +import { page } from 'vitest/browser'; +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'vitest-browser-svelte'; + +vi.mock('$app/forms', () => ({ + enhance: () => () => {} +})); + +vi.mock('$lib/components/tiptap/TipTap.svelte', () => ({ + default: ($anchor: Element) => { + const div = document.createElement('div'); + div.setAttribute('data-testid', 'tiptap-mock'); + $anchor.replaceWith(div); + } +})); + +import ProclamationForm from './ProclamationForm.svelte'; + +const relationOptionsData = { + solfeggioFrequencyId: [ + { label: 'UT - 396', value: 1 }, + { label: 'RE - 417', value: 2 } + ], + subscriptionLevelId: [ + { label: 'Free', value: 1 }, + { label: 'Premium', value: 2 } + ] +}; + +const categories = [ + { id: 1, name: 'Healing', parentId: null }, + { id: 2, name: 'Manifestation', parentId: null }, + { id: 3, name: 'Physical Healing', parentId: 1 }, + { id: 4, name: 'Emotional Healing', parentId: 1 } +]; + +describe('ProclamationForm', () => { + it('renders all basic fields in create mode', async () => { + render(ProclamationForm, { + relationOptionsData, + categories, + isSuper: true + }); + + await expect + .element(page.getByRole('textbox', { name: 'Name' })) + .toBeInTheDocument(); + await expect + .element(page.getByRole('combobox', { name: /^Frequency$/ })) + .toBeInTheDocument(); + await expect + .element(page.getByRole('combobox', { name: /^Category$/ })) + .toBeInTheDocument(); + await expect + .element(page.getByRole('combobox', { name: /^Sub-Category$/ })) + .toBeInTheDocument(); + await expect + .element(page.getByRole('combobox', { name: /^Subscription Level$/ })) + .toBeInTheDocument(); + await expect + .element(page.getByRole('button', { name: 'Save' })) + .toBeInTheDocument(); + }); + + it('pre-fills name input when editing an existing record', async () => { + render(ProclamationForm, { + record: { + id: 'proc-1', + name: 'Test Proclamation', + solfeggioFrequencyId: 2, + subscriptionLevelId: 2, + categoryId: 3, + parentCategoryId: 1 + }, + relationOptionsData, + categories, + isSuper: true, + submitLabel: 'Update' + }); + + await expect + .element(page.getByRole('textbox', { name: 'Name' })) + .toHaveValue('Test Proclamation'); + }); + + it('pre-fills select values when editing an existing record', async () => { + render(ProclamationForm, { + record: { + id: 'proc-1', + name: 'Test Proclamation', + solfeggioFrequencyId: 2, + subscriptionLevelId: 2, + categoryId: 3, + parentCategoryId: 1 + }, + relationOptionsData, + categories, + isSuper: true + }); + + const freqEl = (await page + .getByRole('combobox', { name: /^Frequency$/ }) + .element()) as HTMLSelectElement; + const levelEl = (await page + .getByRole('combobox', { name: /^Subscription Level$/ }) + .element()) as HTMLSelectElement; + + expect(freqEl.value).toBe('2'); + expect(levelEl.value).toBe('2'); + }); + + it('shows content field when isSuper is true', async () => { + render(ProclamationForm, { + record: { id: 'proc-1', name: 'Test', content: '

Secret

' }, + relationOptionsData, + categories, + isSuper: true + }); + + await expect.element(page.getByText('Content')).toBeInTheDocument(); + await expect.element(page.getByTestId('tiptap-mock')).toBeInTheDocument(); + }); + + it('hides content field when isSuper is false', async () => { + render(ProclamationForm, { + record: { id: 'proc-1', name: 'Test', content: '

Secret

' }, + relationOptionsData, + categories, + isSuper: false + }); + + await expect.element(page.getByText('Content')).not.toBeInTheDocument(); + }); + + it('disables submit button when editing and not dirty', async () => { + render(ProclamationForm, { + record: { id: 'proc-1', name: 'Test' }, + relationOptionsData, + categories, + isSuper: true, + submitLabel: 'Update' + }); + + await expect + .element(page.getByRole('button', { name: 'Update' })) + .toBeDisabled(); + }); + + it('enables submit button when editing and user modifies a field', async () => { + render(ProclamationForm, { + record: { id: 'proc-1', name: 'Test' }, + relationOptionsData, + categories, + isSuper: true, + submitLabel: 'Update' + }); + + await page.getByRole('textbox', { name: 'Name' }).fill('New Name'); + + await expect + .element(page.getByRole('button', { name: 'Update' })) + .toBeEnabled(); + }); +}); diff --git a/src/routes/(protected)/admin/proclamations/page.server.spec.ts b/src/routes/(protected)/admin/proclamations/page.server.spec.ts new file mode 100644 index 0000000..fa4dd97 --- /dev/null +++ b/src/routes/(protected)/admin/proclamations/page.server.spec.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { isHttpError } from '@sveltejs/kit'; +import { createLocals, mockUser, createFormRequest } from '$lib/test-utils'; + +vi.mock('$lib/db', () => ({ + prisma: { + proclamation: { + findMany: vi.fn(), + findUnique: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn() + }, + proclamationCategory: { + findMany: vi.fn() + } + } +})); + +vi.mock('$lib/svelte-admin/crud', () => ({ + loadRecords: vi.fn(), + deleteRecord: vi.fn(), + superFormErrorsToFieldErrors: vi.fn(() => ({ error: '', fieldErrors: {} })) +})); + +import { prisma } from '$lib/db'; +import { loadRecords, deleteRecord, superFormErrorsToFieldErrors } from '$lib/svelte-admin/crud'; +import { load, actions } from './+page.server'; + +const mockLoadRecords = loadRecords as ReturnType; +const mockDeleteRecord = deleteRecord as ReturnType; +const mockSuperFormErrorsToFieldErrors = superFormErrorsToFieldErrors as ReturnType; +const mockCatFindMany = prisma.proclamationCategory.findMany as ReturnType; +const mockFindUnique = prisma.proclamation.findUnique as ReturnType; +const mockCreate = prisma.proclamation.create as ReturnType; +const mockUpdate = prisma.proclamation.update as ReturnType; + +describe('proclamations admin page server', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('load', () => { + const recordsWithContent = [ + { id: '1', name: 'A', content: '

secret

' }, + { id: '2', name: 'B', content: '

secret2

' } + ]; + const relationOptionsData = {}; + const categories = [{ id: 1, name: 'Healing', parentId: null }]; + + it('strips content for non-Super users', async () => { + mockLoadRecords.mockResolvedValue({ records: recordsWithContent, relationOptionsData }); + mockCatFindMany.mockResolvedValue(categories); + + const locals = createLocals(mockUser({ roleId: 4 }) as any); + const result = await load({ locals } as any); + + expect(result).toEqual({ + records: [ + { id: '1', name: 'A' }, + { id: '2', name: 'B' } + ], + relationOptionsData, + categories + }); + }); + + it('keeps content for Super users', async () => { + mockLoadRecords.mockResolvedValue({ records: recordsWithContent, relationOptionsData }); + mockCatFindMany.mockResolvedValue(categories); + + const locals = createLocals(mockUser({ roleId: 1 }) as any); + const result = await load({ locals } as any); + + expect(result).toEqual({ + records: recordsWithContent, + relationOptionsData, + categories + }); + }); + }); + + describe('upsert action', () => { + it('rejects unauthenticated requests', async () => { + const locals = createLocals(); + const req = createFormRequest({ name: 'test' }); + + const result = await actions.upsert({ request: req, locals } as any); + expect(result).toEqual({ success: false, error: 'Unauthorized' }); + }); + + it('rejects non-Super users', async () => { + const locals = createLocals(mockUser({ roleId: 4 }) as any); + const req = createFormRequest({ name: 'test' }); + + const result = await actions.upsert({ request: req, locals } as any); + expect(result).toEqual({ + success: false, + error: 'Only Super admins can edit proclamations' + }); + }); + }); + + describe('delete action', () => { + it('rejects non-Super users', async () => { + const locals = createLocals(mockUser({ roleId: 4 }) as any); + const req = new Request('http://localhost', { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ id: '1' }) + }); + + const result = await actions.delete({ request: req, locals } as any); + expect(result).toEqual({ + success: false, + error: 'Only Super admins can delete proclamations' + }); + }); + + it('delegates to deleteRecord for Super users', async () => { + mockDeleteRecord.mockResolvedValue({ success: true }); + + const locals = createLocals(mockUser({ roleId: 1 }) as any); + const req = new Request('http://localhost', { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ id: '1' }) + }); + + const result = await actions.delete({ request: req, locals } as any); + expect(mockDeleteRecord).toHaveBeenCalled(); + expect(result).toEqual({ success: true }); + }); + }); +});