From b8bc096adba54c220a204b542921d004fe1845be Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 6 Apr 2026 11:05:42 -0700 Subject: [PATCH] test(smart-actions): add scheduler, webhook, copilot integration tests --- .../modules/note-prompts/scheduler.test.ts | 412 ++++++++++++++++++ backend/src/modules/notes/copilot.test.ts | 237 ++++++++++ web/e2e/smart-actions.spec.ts | 133 ++++++ 3 files changed, 782 insertions(+) create mode 100644 backend/src/modules/note-prompts/scheduler.test.ts create mode 100644 backend/src/modules/notes/copilot.test.ts create mode 100644 web/e2e/smart-actions.spec.ts diff --git a/backend/src/modules/note-prompts/scheduler.test.ts b/backend/src/modules/note-prompts/scheduler.test.ts new file mode 100644 index 0000000..20d8e35 --- /dev/null +++ b/backend/src/modules/note-prompts/scheduler.test.ts @@ -0,0 +1,412 @@ +/** + * Integration tests for prompt scheduler + webhook CRUD routes. + * Uses buildTestApp() + Fastify .inject() for real request/response flows. + */ + +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { FastifyInstance } from 'fastify'; + +vi.mock('../../lib/auth.js', () => ({ + extractAuth: vi.fn(async () => ({ sub: 'user_1', type: 'access', role: 'editor' })), + requireWriter: vi.fn(async () => ({ sub: 'user_1', type: 'access', role: 'editor' })), +})); +vi.mock('../../lib/product-config.js', () => ({ + PRODUCT_ID: 'notelett', + DISPLAY_NAME: 'NoteLett', + productConfig: { productId: 'notelett', displayName: 'NoteLett' }, +})); +vi.mock('../../lib/request-context.js', () => ({ + getUserId: vi.fn(() => 'user_1'), + getRequestProductId: vi.fn(() => 'notelett'), +})); +vi.mock('../../lib/telemetry.js', () => ({ trackEvent: vi.fn() })); +vi.mock('../../lib/feature-flags.js', () => ({ isFeatureEnabled: vi.fn(() => true) })); +vi.mock('../../lib/field-encrypt.js', () => ({ + initEncryption: vi.fn(), + getEncryptor: vi.fn(() => ({ + encrypt: vi.fn(async (v: unknown) => v), + decrypt: vi.fn(async (v: unknown) => v), + })), +})); +vi.mock('../../lib/llm.js', () => ({ + llm: vi.fn(() => ({ + isConfigured: () => true, + chatCompletion: vi.fn(async () => ({ + content: 'Mock LLM response', + model: 'mock-model', + usage: { promptTokens: 10, completionTokens: 10, totalTokens: 20 }, + finishReason: 'stop', + })), + })), + initLLM: vi.fn(), +})); +vi.mock('../../lib/embeddings.js', () => ({ + embedText: vi.fn(async () => null), + cosineSimilarity: vi.fn(() => 0), + stripHtmlForEmbedding: vi.fn((html: string) => html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()), +})); +vi.mock('@bytelyst/llm', () => ({ + getLLM: vi.fn(), + setLLM: vi.fn(), + createLLMProvider: vi.fn(), + buildVisionMessage: vi.fn((text: string, url: string) => ({ + role: 'user', + content: [{ type: 'text', text }, { type: 'image_url', image_url: { url, detail: 'auto' } }], + })), + hasVisionContent: vi.fn(() => false), + isVisionMessage: vi.fn(() => false), + getMessageText: vi.fn((msg: { content: string }) => typeof msg.content === 'string' ? msg.content : ''), +})); + +import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js'; +import { promptSchedulerRoutes } from './scheduler.js'; +import { notePromptRoutes } from './routes.js'; +import { noteRoutes } from '../notes/routes.js'; +import { upsertBuiltinTemplate } from './repository.js'; +import { getBuiltinTemplates } from './seed.js'; + +let app: FastifyInstance; + +beforeAll(async () => { + app = await buildTestApp(async (fastify) => { + await noteRoutes(fastify); + await notePromptRoutes(fastify); + await promptSchedulerRoutes(fastify); + }); +}); + +afterAll(async () => { + await app.close(); +}); + +async function seedBuiltins() { + for (const t of getBuiltinTemplates()) { + await upsertBuiltinTemplate(t); + } +} + +// ── Schedule CRUD ────────────────────────────────────────────────── + +describe('prompt-schedules CRUD', () => { + beforeEach(() => { + resetMemoryDatastore(); + }); + + it('POST /prompt-schedules — creates a schedule', async () => { + await seedBuiltins(); + const res = await app.inject({ + method: 'POST', + url: '/api/prompt-schedules', + payload: { + workspaceId: 'ws1', + templateId: 'builtin-summarize', + name: 'Weekly Digest', + cron: '0 9 * * 1', + enabled: true, + }, + }); + expect(res.statusCode).toBe(201); + const body = res.json(); + expect(body.name).toBe('Weekly Digest'); + expect(body.cron).toBe('0 9 * * 1'); + expect(body.productId).toBe('notelett'); + expect(body.userId).toBe('user_1'); + expect(body.enabled).toBe(true); + expect(body.lastRunAt).toBeNull(); + expect(body.id).toBeTruthy(); + }); + + it('GET /prompt-schedules — lists schedules', async () => { + await app.inject({ + method: 'POST', + url: '/api/prompt-schedules', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'S1', cron: '0 9 * * *' }, + }); + await app.inject({ + method: 'POST', + url: '/api/prompt-schedules', + payload: { workspaceId: 'ws1', templateId: 't2', name: 'S2', cron: '0 10 * * *' }, + }); + + const res = await app.inject({ method: 'GET', url: '/api/prompt-schedules' }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.items.length).toBe(2); + expect(body.total).toBe(2); + }); + + it('GET /prompt-schedules/:id — returns a single schedule', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/prompt-schedules', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'Get Test', cron: '0 8 * * *' }, + }); + const id = createRes.json().id; + + const res = await app.inject({ method: 'GET', url: `/api/prompt-schedules/${id}` }); + expect(res.statusCode).toBe(200); + expect(res.json().name).toBe('Get Test'); + }); + + it('GET /prompt-schedules/:id — 404 for unknown id', async () => { + const res = await app.inject({ method: 'GET', url: '/api/prompt-schedules/nonexistent' }); + expect(res.statusCode).toBe(404); + }); + + it('PATCH /prompt-schedules/:id — updates schedule', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/prompt-schedules', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'Original', cron: '0 8 * * *' }, + }); + const id = createRes.json().id; + + const res = await app.inject({ + method: 'PATCH', + url: `/api/prompt-schedules/${id}`, + payload: { name: 'Updated', enabled: false }, + }); + expect(res.statusCode).toBe(200); + expect(res.json().name).toBe('Updated'); + expect(res.json().enabled).toBe(false); + }); + + it('DELETE /prompt-schedules/:id — deletes schedule', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/prompt-schedules', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'To Delete', cron: '0 8 * * *' }, + }); + const id = createRes.json().id; + + const delRes = await app.inject({ method: 'DELETE', url: `/api/prompt-schedules/${id}` }); + expect(delRes.statusCode).toBe(204); + + const getRes = await app.inject({ method: 'GET', url: `/api/prompt-schedules/${id}` }); + expect(getRes.statusCode).toBe(404); + }); + + it('DELETE /prompt-schedules/:id — 404 for unknown id', async () => { + const res = await app.inject({ method: 'DELETE', url: '/api/prompt-schedules/nonexistent' }); + expect(res.statusCode).toBe(404); + }); +}); + +// ── Webhook CRUD ────────────────────────────────────────────────── + +describe('prompt-webhooks CRUD', () => { + beforeEach(() => { + resetMemoryDatastore(); + }); + + it('POST /prompt-webhooks — creates a webhook', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { + workspaceId: 'ws1', + templateId: 't1', + name: 'On Note Create', + triggerEvent: 'note.created', + enabled: true, + }, + }); + expect(res.statusCode).toBe(201); + const body = res.json(); + expect(body.name).toBe('On Note Create'); + expect(body.triggerEvent).toBe('note.created'); + expect(body.productId).toBe('notelett'); + expect(body.lastTriggeredAt).toBeNull(); + }); + + it('GET /prompt-webhooks — lists webhooks', async () => { + await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'W1', triggerEvent: 'note.created' }, + }); + await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { workspaceId: 'ws1', templateId: 't2', name: 'W2', triggerEvent: 'note.updated' }, + }); + + const res = await app.inject({ method: 'GET', url: '/api/prompt-webhooks' }); + expect(res.statusCode).toBe(200); + expect(res.json().items.length).toBe(2); + }); + + it('GET /prompt-webhooks/:id — returns single webhook', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'Fetch Test', triggerEvent: 'note.tagged' }, + }); + const id = createRes.json().id; + + const res = await app.inject({ method: 'GET', url: `/api/prompt-webhooks/${id}` }); + expect(res.statusCode).toBe(200); + expect(res.json().name).toBe('Fetch Test'); + }); + + it('PATCH /prompt-webhooks/:id — updates webhook', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'Original WH', triggerEvent: 'external' }, + }); + const id = createRes.json().id; + + const res = await app.inject({ + method: 'PATCH', + url: `/api/prompt-webhooks/${id}`, + payload: { name: 'Updated WH', enabled: false }, + }); + expect(res.statusCode).toBe(200); + expect(res.json().name).toBe('Updated WH'); + expect(res.json().enabled).toBe(false); + }); + + it('DELETE /prompt-webhooks/:id — deletes webhook', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'Del WH', triggerEvent: 'note.created' }, + }); + const id = createRes.json().id; + + const delRes = await app.inject({ method: 'DELETE', url: `/api/prompt-webhooks/${id}` }); + expect(delRes.statusCode).toBe(204); + + const getRes = await app.inject({ method: 'GET', url: `/api/prompt-webhooks/${id}` }); + expect(getRes.statusCode).toBe(404); + }); +}); + +// ── Webhook Trigger ────────────────────────────────────────────── + +describe('prompt-webhooks trigger', () => { + beforeEach(() => { + resetMemoryDatastore(); + }); + + it('POST /prompt-webhooks/:id/trigger — executes prompt on note', async () => { + await seedBuiltins(); + + // Create a note + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'wh-note-1', workspaceId: 'ws-wh', title: 'Webhook Target', body: 'Some content to process.' }, + }); + + // Create webhook linked to builtin summarize + const whRes = await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { + workspaceId: 'ws-wh', + templateId: 'builtin-summarize', + name: 'Trigger Test', + triggerEvent: 'external', + enabled: true, + }, + }); + const whId = whRes.json().id; + + // Trigger it + const triggerRes = await app.inject({ + method: 'POST', + url: `/api/prompt-webhooks/${whId}/trigger`, + payload: { noteId: 'wh-note-1', workspaceId: 'ws-wh' }, + }); + expect(triggerRes.statusCode).toBe(200); + const body = triggerRes.json(); + expect(body.triggered).toBe(true); + expect(body.webhookId).toBe(whId); + expect(body.result.content).toBeTruthy(); + }); + + it('POST /prompt-webhooks/:id/trigger — 404 for disabled webhook', async () => { + await seedBuiltins(); + + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'wh-note-2', workspaceId: 'ws-wh2', title: 'T', body: 'B' }, + }); + + const whRes = await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { + workspaceId: 'ws-wh2', + templateId: 'builtin-summarize', + name: 'Disabled', + triggerEvent: 'external', + enabled: false, + }, + }); + const whId = whRes.json().id; + + const triggerRes = await app.inject({ + method: 'POST', + url: `/api/prompt-webhooks/${whId}/trigger`, + payload: { noteId: 'wh-note-2', workspaceId: 'ws-wh2' }, + }); + expect(triggerRes.statusCode).toBe(404); + }); + + it('POST /prompt-webhooks/:id/trigger — 404 for nonexistent note', async () => { + await seedBuiltins(); + + const whRes = await app.inject({ + method: 'POST', + url: '/api/prompt-webhooks', + payload: { + workspaceId: 'ws-wh3', + templateId: 'builtin-summarize', + name: 'Bad Note', + triggerEvent: 'external', + enabled: true, + }, + }); + const whId = whRes.json().id; + + const triggerRes = await app.inject({ + method: 'POST', + url: `/api/prompt-webhooks/${whId}/trigger`, + payload: { noteId: 'nonexistent-note', workspaceId: 'ws-wh3' }, + }); + expect(triggerRes.statusCode).toBe(404); + }); +}); + +// ── Scheduler diagnostics ──────────────────────────────────────── + +describe('scheduler diagnostics', () => { + beforeEach(() => { + resetMemoryDatastore(); + }); + + it('GET /prompt-schedules/diagnostics — returns scheduler state', async () => { + await app.inject({ + method: 'POST', + url: '/api/prompt-schedules', + payload: { workspaceId: 'ws1', templateId: 't1', name: 'Active', cron: '0 9 * * *', enabled: true }, + }); + await app.inject({ + method: 'POST', + url: '/api/prompt-schedules', + payload: { workspaceId: 'ws1', templateId: 't2', name: 'Disabled', cron: '0 10 * * *', enabled: false }, + }); + + const res = await app.inject({ method: 'GET', url: '/api/prompt-schedules/diagnostics' }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.totalSchedules).toBe(2); + expect(body.enabled).toBe(1); + expect(typeof body.dueNow).toBe('number'); + expect(Array.isArray(body.nextRuns)).toBe(true); + }); +}); diff --git a/backend/src/modules/notes/copilot.test.ts b/backend/src/modules/notes/copilot.test.ts new file mode 100644 index 0000000..377697d --- /dev/null +++ b/backend/src/modules/notes/copilot.test.ts @@ -0,0 +1,237 @@ +/** + * Integration tests for copilot transform routes (POST /notes/:id/copilot). + * Uses buildTestApp() + Fastify .inject() for real request/response flows. + */ + +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { FastifyInstance } from 'fastify'; + +const { + extractAuthMock, + runCopilotTransformMock, + suggestTitleFromBodyMock, +} = vi.hoisted(() => ({ + extractAuthMock: vi.fn(async () => ({ sub: 'user_1', type: 'access', role: 'editor' })), + runCopilotTransformMock: vi.fn(async (_action: string, text: string) => `transformed: ${text.slice(0, 30)}`), + suggestTitleFromBodyMock: vi.fn(async () => 'AI Suggested Title'), +})); + +vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock, requireWriter: extractAuthMock })); +vi.mock('../../lib/product-config.js', () => ({ + PRODUCT_ID: 'notelett', + DISPLAY_NAME: 'NoteLett', + productConfig: { productId: 'notelett', displayName: 'NoteLett' }, +})); +vi.mock('../../lib/request-context.js', () => ({ + getUserId: vi.fn(() => 'user_1'), + getRequestProductId: vi.fn(() => 'notelett'), +})); +vi.mock('../../lib/telemetry.js', () => ({ trackEvent: vi.fn() })); +vi.mock('../../lib/feature-flags.js', () => ({ + isFeatureEnabled: vi.fn(() => true), +})); +vi.mock('../../lib/field-encrypt.js', () => ({ + initEncryption: vi.fn(), + getEncryptor: vi.fn(() => ({ + encrypt: vi.fn(async (v: unknown) => v), + decrypt: vi.fn(async (v: unknown) => v), + })), +})); +vi.mock('../../lib/extraction-client.js', () => ({ + extractFromText: vi.fn(async () => ({ summary: 'test summary' })), +})); +vi.mock('../../lib/copilot-transform.js', () => ({ + runCopilotTransform: runCopilotTransformMock, + suggestTitleFromBody: suggestTitleFromBodyMock, +})); +vi.mock('../note-artifacts/repository.js', () => ({ + createNoteArtifact: vi.fn(async (doc: unknown) => doc), +})); +vi.mock('../note-versions/repository.js', () => ({ + appendNoteVersion: vi.fn(async () => ({})), + listNoteVersions: vi.fn(async () => ({ items: [], total: 0 })), +})); +vi.mock('../note-shares/repository.js', () => ({ + createNoteShare: vi.fn(async () => ({})), +})); + +import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js'; +import { noteRoutes } from './routes.js'; + +let app: FastifyInstance; + +beforeAll(async () => { + app = await buildTestApp(async (fastify) => { + await noteRoutes(fastify); + }); +}); + +afterAll(async () => { + await app.close(); +}); + +describe('POST /notes/:id/copilot', () => { + beforeEach(() => { + resetMemoryDatastore(); + vi.clearAllMocks(); + }); + + it('shorten action — transforms text and returns result', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'cop-1', workspaceId: 'ws-cop', title: 'Copilot Note', body: 'Long paragraph about AI.' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/cop-1/copilot', + payload: { workspaceId: 'ws-cop', action: 'shorten', text: 'Long paragraph about AI that needs shortening.' }, + }); + expect(res.statusCode).toBe(200); + expect(res.json().text).toContain('transformed:'); + expect(runCopilotTransformMock).toHaveBeenCalledWith('shorten', 'Long paragraph about AI that needs shortening.'); + }); + + it('expand action — calls transform with expand', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'cop-2', workspaceId: 'ws-cop', title: 'Short Note', body: 'Brief.' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/cop-2/copilot', + payload: { workspaceId: 'ws-cop', action: 'expand', text: 'Brief.' }, + }); + expect(res.statusCode).toBe(200); + expect(runCopilotTransformMock).toHaveBeenCalledWith('expand', 'Brief.'); + }); + + it('bulletize action — calls transform with bulletize', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'cop-3', workspaceId: 'ws-cop', title: 'List Note', body: 'Items to list.' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/cop-3/copilot', + payload: { workspaceId: 'ws-cop', action: 'bulletize', text: 'First point. Second point. Third point.' }, + }); + expect(res.statusCode).toBe(200); + expect(runCopilotTransformMock).toHaveBeenCalledWith('bulletize', 'First point. Second point. Third point.'); + }); + + it('grammar action — calls transform with grammar', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'cop-4', workspaceId: 'ws-cop', title: 'Grammar Note', body: 'Typos here.' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/cop-4/copilot', + payload: { workspaceId: 'ws-cop', action: 'grammar', text: 'Ths haz typos in it.' }, + }); + expect(res.statusCode).toBe(200); + expect(runCopilotTransformMock).toHaveBeenCalledWith('grammar', 'Ths haz typos in it.'); + }); + + it('change-tone with tone parameter — appends tone to text', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'cop-5', workspaceId: 'ws-cop', title: 'Tone Note', body: 'Casual text.' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/cop-5/copilot', + payload: { workspaceId: 'ws-cop', action: 'change-tone', text: 'Hey whats up', tone: 'formal' }, + }); + expect(res.statusCode).toBe(200); + expect(runCopilotTransformMock).toHaveBeenCalledWith('change-tone', 'Hey whats up\n\nTone: formal'); + }); + + it('404 for nonexistent note', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/notes/nonexistent/copilot', + payload: { workspaceId: 'ws-cop', action: 'shorten', text: 'text' }, + }); + expect(res.statusCode).toBe(404); + }); + + it('400 for invalid action', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'cop-6', workspaceId: 'ws-cop', title: 'T', body: 'B' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/cop-6/copilot', + payload: { workspaceId: 'ws-cop', action: 'invalid-action', text: 'text' }, + }); + expect(res.statusCode).toBe(400); + }); + + it('400 for missing text', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'cop-7', workspaceId: 'ws-cop', title: 'T', body: 'B' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/cop-7/copilot', + payload: { workspaceId: 'ws-cop', action: 'shorten' }, + }); + expect(res.statusCode).toBe(400); + }); +}); + +describe('POST /notes/:id/suggest-title', () => { + beforeEach(() => { + resetMemoryDatastore(); + vi.clearAllMocks(); + }); + + it('returns suggested title from body', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'st-1', workspaceId: 'ws-st', title: 'Original', body: 'A long body about machine learning.' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/st-1/suggest-title', + payload: { workspaceId: 'ws-st' }, + }); + expect(res.statusCode).toBe(200); + expect(res.json().title).toBe('AI Suggested Title'); + expect(suggestTitleFromBodyMock).toHaveBeenCalled(); + }); + + it('400 without workspaceId', async () => { + await app.inject({ + method: 'POST', + url: '/api/notes', + payload: { id: 'st-2', workspaceId: 'ws-st', title: 'T', body: 'B' }, + }); + + const res = await app.inject({ + method: 'POST', + url: '/api/notes/st-2/suggest-title', + payload: {}, + }); + expect(res.statusCode).toBe(400); + }); +}); diff --git a/web/e2e/smart-actions.spec.ts b/web/e2e/smart-actions.spec.ts new file mode 100644 index 0000000..d983f26 --- /dev/null +++ b/web/e2e/smart-actions.spec.ts @@ -0,0 +1,133 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Smart Actions", () => { + test.beforeEach(async ({ page }) => { + // Mock all backend API calls + await page.route("**/api/note-prompts**", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + items: [ + { + id: "builtin-summarize", + slug: "summarize", + name: "Summarize", + category: "transform", + isBuiltin: true, + inputType: "text", + outputType: "new_note", + }, + { + id: "custom-1", + slug: "my-custom", + name: "My Custom Template", + category: "generate", + isBuiltin: false, + inputType: "text", + outputType: "replace", + }, + ], + total: 2, + }), + }); + } + if (route.request().method() === "POST") { + return route.fulfill({ + status: 201, + contentType: "application/json", + body: JSON.stringify({ + id: "new-template-1", + slug: "new-template", + name: "New Template", + category: "transform", + isBuiltin: false, + }), + }); + } + if (route.request().method() === "DELETE") { + return route.fulfill({ status: 204 }); + } + return route.fulfill({ status: 200, contentType: "application/json", body: "{}" }); + }); + + await page.route("**/api/notes**", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ items: [], total: 0 }), + }) + ); + + await page.route("**/api/workspaces**", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ items: [], total: 0 }), + }) + ); + + await page.route("**/api/prompt-schedules**", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ items: [], total: 0 }), + }) + ); + + await page.route("**/api/prompt-webhooks**", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ items: [], total: 0 }), + }) + ); + + await page.route("**/api/**", (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }) + ); + }); + + test("dashboard page loads with smart actions API mocked", async ({ page }) => { + await page.goto("/dashboard"); + await page.waitForLoadState("domcontentloaded"); + await expect(page.locator("body")).toBeVisible(); + }); + + test("note detail page loads without JS errors", async ({ page }) => { + const errors: string[] = []; + page.on("pageerror", (err) => errors.push(err.message)); + + await page.goto("/notes/test-note-1"); + await page.waitForLoadState("domcontentloaded"); + await expect(page.locator("body")).toBeVisible(); + + const realErrors = errors.filter( + (e) => + !e.includes("fetch") && + !e.includes("Failed") && + !e.includes("Unexpected") && + !e.includes("hydration") + ); + expect(realErrors).toHaveLength(0); + }); + + test("settings page loads without JS errors", async ({ page }) => { + const errors: string[] = []; + page.on("pageerror", (err) => errors.push(err.message)); + + await page.goto("/settings"); + await page.waitForLoadState("domcontentloaded"); + await expect(page.locator("body")).toBeVisible(); + + const realErrors = errors.filter( + (e) => + !e.includes("fetch") && + !e.includes("Failed") && + !e.includes("Unexpected") && + !e.includes("hydration") + ); + expect(realErrors).toHaveLength(0); + }); +});