// @vitest-environment jsdom import { cleanup, render, waitFor } from '@testing-library/react'; import type { ReactNode } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ProjectView } from '../../src/components/ProjectView'; import { useIframeKeepAlivePool } from '../../src/components/IframeKeepAlivePool'; import type { AgentInfo, AppConfig, Conversation, DesignSystemSummary, Project, SkillSummary, } from '../../src/types'; import { createConversation, listConversations, listMessages, loadTabs, } from '../../src/state/projects'; import { fetchPreviewComments } from '../../src/providers/registry'; const evictProjectMock = vi.fn(); vi.mock('../../src/i18n', () => ({ useT: () => (key: string) => key, useI18n: () => ({ t: (key: string) => key, locale: 'en', setLocale: () => {}, }), })); vi.mock('../../src/router', () => ({ navigate: vi.fn(), })); vi.mock('../../src/components/IframeKeepAlivePool', async () => { const actual = await vi.importActual( '../../src/components/IframeKeepAlivePool', ); return { ...actual, useIframeKeepAlivePool: vi.fn(), }; }); vi.mock('../../src/providers/anthropic', () => ({ streamMessage: vi.fn(), })); vi.mock('../../src/providers/daemon', () => ({ fetchChatRunStatus: vi.fn(), listActiveChatRuns: vi.fn().mockResolvedValue([]), reattachDaemonRun: vi.fn(), streamViaDaemon: vi.fn(), })); vi.mock('../../src/providers/project-events', () => ({ useProjectFileEvents: vi.fn(), })); vi.mock('../../src/providers/registry', async () => { const actual = await vi.importActual( '../../src/providers/registry', ); return { ...actual, deletePreviewComment: vi.fn(), fetchDesignSystem: vi.fn(), fetchLiveArtifacts: vi.fn().mockResolvedValue([]), fetchPreviewComments: vi.fn(), fetchProjectFiles: vi.fn().mockResolvedValue([]), fetchSkill: vi.fn(), getTemplate: vi.fn(), patchPreviewCommentStatus: vi.fn(), upsertPreviewComment: vi.fn(), writeProjectTextFile: vi.fn(), }; }); vi.mock('../../src/state/projects', async () => { const actual = await vi.importActual( '../../src/state/projects', ); return { ...actual, createConversation: vi.fn(), listConversations: vi.fn(), listMessages: vi.fn(), loadTabs: vi.fn(), patchConversation: vi.fn(), patchProject: vi.fn(), saveMessage: vi.fn(), saveTabs: vi.fn(), }; }); vi.mock('../../src/components/AppChromeHeader', () => ({ AppChromeHeader: ({ children }: { children: ReactNode }) =>
{children}
, })); vi.mock('../../src/components/AvatarMenu', () => ({ AvatarMenu: () => null, })); vi.mock('../../src/components/FileWorkspace', () => ({ DESIGN_SYSTEM_TAB: '__design_system__', FileWorkspace: () =>
, })); vi.mock('../../src/components/Loading', () => ({ CenteredLoader: () =>
, })); vi.mock('../../src/components/ChatPane', () => ({ ChatPane: () =>
, })); const mockedUseIframeKeepAlivePool = vi.mocked(useIframeKeepAlivePool); const mockedListConversations = vi.mocked(listConversations); const mockedCreateConversation = vi.mocked(createConversation); const mockedListMessages = vi.mocked(listMessages); const mockedLoadTabs = vi.mocked(loadTabs); const mockedFetchPreviewComments = vi.mocked(fetchPreviewComments); const config: AppConfig = { mode: 'api', apiKey: '', baseUrl: '', model: '', agentId: null, skillId: null, designSystemId: null, }; const project: Project = { id: 'project-1', name: 'Project 1', skillId: 'skill-1', designSystemId: 'ds-1', createdAt: 1, updatedAt: 1, }; const conversation: Conversation = { id: 'conv-1', projectId: project.id, title: null, createdAt: 1, updatedAt: 1, }; const skill: SkillSummary = { id: 'skill-1', name: 'Prompt skill', description: 'Old prompt context', triggers: ['prompt'], mode: 'prototype', previewType: 'html', designSystemRequired: false, defaultFor: [], upstream: null, hasBody: true, examplePrompt: 'Create a prototype.', aggregatesExamples: false, }; const designSystem: DesignSystemSummary = { id: 'ds-1', title: 'Design System', category: 'product', summary: 'Old system context', }; function renderProjectView(props?: { skills?: SkillSummary[]; designSystems?: DesignSystemSummary[]; }) { return render( , ); } describe('ProjectView preview keep-alive invalidation', () => { beforeEach(() => { mockedUseIframeKeepAlivePool.mockReturnValue({ attach: vi.fn(), release: vi.fn(), evict: vi.fn(), evictProject: evictProjectMock, evictMatching: vi.fn(), }); mockedListConversations.mockResolvedValue([conversation]); mockedCreateConversation.mockResolvedValue(conversation); mockedListMessages.mockResolvedValue([]); mockedLoadTabs.mockResolvedValue({ tabs: [], active: null }); mockedFetchPreviewComments.mockResolvedValue([]); }); afterEach(() => { cleanup(); vi.clearAllMocks(); }); it('evicts the active preview when registry prompt context changes for the open project', async () => { const view = renderProjectView(); view.rerender( , ); await waitFor(() => { expect(evictProjectMock).toHaveBeenCalledWith('project-1', { includeActive: true }); }); }); });