From c12f0d16bb20572906faf19e0eba85528756b110 Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Thu, 18 Dec 2025 17:47:13 +0800
Subject: [PATCH] chore(web): enhance frontend tests (#29869)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
.github/workflows/web-tests.yml | 108 +++--
.../assistant-type-picker/index.spec.tsx | 204 +++++----
.../debug-with-single-model/index.spec.tsx | 399 +++++++++---------
.../billing/upgrade-btn/index.spec.tsx | 174 ++------
.../explore/installed-app/index.spec.tsx | 77 +---
web/jest.config.ts | 1 +
web/jest.setup.ts | 16 +
web/package.json | 1 +
web/pnpm-lock.yaml | 3 +
9 files changed, 434 insertions(+), 549 deletions(-)
diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml
index dd311701b5..8b871403cc 100644
--- a/.github/workflows/web-tests.yml
+++ b/.github/workflows/web-tests.yml
@@ -70,6 +70,13 @@ jobs:
node <<'NODE' >> "$GITHUB_STEP_SUMMARY"
const fs = require('fs');
const path = require('path');
+ let libCoverage = null;
+
+ try {
+ libCoverage = require('istanbul-lib-coverage');
+ } catch (error) {
+ libCoverage = null;
+ }
const summaryPath = path.join('coverage', 'coverage-summary.json');
const finalPath = path.join('coverage', 'coverage-final.json');
@@ -91,6 +98,54 @@ jobs:
? JSON.parse(fs.readFileSync(finalPath, 'utf8'))
: null;
+ const getLineCoverageFromStatements = (statementMap, statementHits) => {
+ const lineHits = {};
+
+ if (!statementMap || !statementHits) {
+ return lineHits;
+ }
+
+ Object.entries(statementMap).forEach(([key, statement]) => {
+ const line = statement?.start?.line;
+ if (!line) {
+ return;
+ }
+ const hits = statementHits[key] ?? 0;
+ const previous = lineHits[line];
+ lineHits[line] = previous === undefined ? hits : Math.max(previous, hits);
+ });
+
+ return lineHits;
+ };
+
+ const getFileCoverage = (entry) => (
+ libCoverage ? libCoverage.createFileCoverage(entry) : null
+ );
+
+ const getLineHits = (entry, fileCoverage) => {
+ const lineHits = entry.l ?? {};
+ if (Object.keys(lineHits).length > 0) {
+ return lineHits;
+ }
+ if (fileCoverage) {
+ return fileCoverage.getLineCoverage();
+ }
+ return getLineCoverageFromStatements(entry.statementMap ?? {}, entry.s ?? {});
+ };
+
+ const getUncoveredLines = (entry, fileCoverage, lineHits) => {
+ if (lineHits && Object.keys(lineHits).length > 0) {
+ return Object.entries(lineHits)
+ .filter(([, count]) => count === 0)
+ .map(([line]) => Number(line))
+ .sort((a, b) => a - b);
+ }
+ if (fileCoverage) {
+ return fileCoverage.getUncoveredLines();
+ }
+ return [];
+ };
+
const totals = {
lines: { covered: 0, total: 0 },
statements: { covered: 0, total: 0 },
@@ -106,7 +161,7 @@ jobs:
totals[key].covered = totalEntry[key].covered ?? 0;
totals[key].total = totalEntry[key].total ?? 0;
}
- });
+ });
Object.entries(summary)
.filter(([file]) => file !== 'total')
@@ -122,7 +177,8 @@ jobs:
});
} else if (coverage) {
Object.entries(coverage).forEach(([file, entry]) => {
- const lineHits = entry.l ?? {};
+ const fileCoverage = getFileCoverage(entry);
+ const lineHits = getLineHits(entry, fileCoverage);
const statementHits = entry.s ?? {};
const branchHits = entry.b ?? {};
const functionHits = entry.f ?? {};
@@ -228,7 +284,8 @@ jobs:
};
const tableRows = Object.entries(coverage)
.map(([file, entry]) => {
- const lineHits = entry.l ?? {};
+ const fileCoverage = getFileCoverage(entry);
+ const lineHits = getLineHits(entry, fileCoverage);
const statementHits = entry.s ?? {};
const branchHits = entry.b ?? {};
const functionHits = entry.f ?? {};
@@ -254,10 +311,7 @@ jobs:
tableTotals.functions.total += functionTotal;
tableTotals.functions.covered += functionCovered;
- const uncoveredLines = Object.entries(lineHits)
- .filter(([, count]) => count === 0)
- .map(([line]) => Number(line))
- .sort((a, b) => a - b);
+ const uncoveredLines = getUncoveredLines(entry, fileCoverage, lineHits);
const filePath = entry.path ?? file;
const relativePath = path.isAbsolute(filePath)
@@ -294,46 +348,20 @@ jobs:
};
const rowsForOutput = [allFilesRow, ...tableRows];
- const columnWidths = Object.fromEntries(
- columns.map(({ key, header }) => [key, header.length]),
- );
-
- rowsForOutput.forEach((row) => {
- columns.forEach(({ key }) => {
- const value = String(row[key] ?? '');
- columnWidths[key] = Math.max(columnWidths[key], value.length);
- });
- });
-
- const formatRow = (row) => columns
- .map(({ key, align }) => {
- const value = String(row[key] ?? '');
- const width = columnWidths[key];
- return align === 'right' ? value.padStart(width) : value.padEnd(width);
- })
- .join(' | ');
-
- const headerRow = columns
- .map(({ header, key, align }) => {
- const width = columnWidths[key];
- return align === 'right' ? header.padStart(width) : header.padEnd(width);
- })
- .join(' | ');
-
- const dividerRow = columns
- .map(({ key }) => '-'.repeat(columnWidths[key]))
- .join('|');
+ const formatRow = (row) => `| ${columns
+ .map(({ key }) => String(row[key] ?? ''))
+ .join(' | ')} |`;
+ const headerRow = `| ${columns.map(({ header }) => header).join(' | ')} |`;
+ const dividerRow = `| ${columns
+ .map(({ align }) => (align === 'right' ? '---:' : ':---'))
+ .join(' | ')} |`;
console.log('');
console.log('Jest coverage table
');
console.log('');
- console.log('```');
- console.log(dividerRow);
console.log(headerRow);
console.log(dividerRow);
rowsForOutput.forEach((row) => console.log(formatRow(row)));
- console.log(dividerRow);
- console.log('```');
console.log(' ');
}
NODE
diff --git a/web/app/components/app/configuration/config/assistant-type-picker/index.spec.tsx b/web/app/components/app/configuration/config/assistant-type-picker/index.spec.tsx
index f935a203fe..cda24ea045 100644
--- a/web/app/components/app/configuration/config/assistant-type-picker/index.spec.tsx
+++ b/web/app/components/app/configuration/config/assistant-type-picker/index.spec.tsx
@@ -5,31 +5,6 @@ import AssistantTypePicker from './index'
import type { AgentConfig } from '@/models/debug'
import { AgentStrategy } from '@/types/app'
-// Type definition for AgentSetting props
-type AgentSettingProps = {
- isChatModel: boolean
- payload: AgentConfig
- isFunctionCall: boolean
- onCancel: () => void
- onSave: (payload: AgentConfig) => void
-}
-
-// Track mock calls for props validation
-let mockAgentSettingProps: AgentSettingProps | null = null
-
-// Mock AgentSetting component (complex modal with external hooks)
-jest.mock('../agent/agent-setting', () => {
- return function MockAgentSetting(props: AgentSettingProps) {
- mockAgentSettingProps = props
- return (
-
-
-
-
- )
- }
-})
-
// Test utilities
const defaultAgentConfig: AgentConfig = {
enabled: true,
@@ -62,7 +37,6 @@ const getOptionByDescription = (descriptionRegex: RegExp) => {
describe('AssistantTypePicker', () => {
beforeEach(() => {
jest.clearAllMocks()
- mockAgentSettingProps = null
})
// Rendering tests (REQUIRED)
@@ -139,8 +113,8 @@ describe('AssistantTypePicker', () => {
renderComponent()
// Act
- const trigger = screen.getByText(/chatAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/chatAssistant.name/i)
+ await user.click(trigger)
// Assert - Both options should be visible
await waitFor(() => {
@@ -225,8 +199,8 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'chat' })
// Act - Open dropdown
- const trigger = screen.getByText(/chatAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/chatAssistant.name/i)
+ await user.click(trigger)
// Wait for dropdown and select agent
await waitFor(() => {
@@ -235,7 +209,7 @@ describe('AssistantTypePicker', () => {
})
const agentOptions = screen.getAllByText(/agentAssistant.name/i)
- await user.click(agentOptions[0].closest('div')!)
+ await user.click(agentOptions[0])
// Assert - Dropdown should remain open (agent settings should be visible)
await waitFor(() => {
@@ -250,8 +224,8 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'chat', onChange })
// Act - Open dropdown
- const trigger = screen.getByText(/chatAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/chatAssistant.name/i)
+ await user.click(trigger)
// Wait for dropdown and click same option
await waitFor(() => {
@@ -260,7 +234,7 @@ describe('AssistantTypePicker', () => {
})
const chatOptions = screen.getAllByText(/chatAssistant.name/i)
- await user.click(chatOptions[1].closest('div')!)
+ await user.click(chatOptions[1])
// Assert
expect(onChange).not.toHaveBeenCalled()
@@ -276,8 +250,8 @@ describe('AssistantTypePicker', () => {
renderComponent({ disabled: true, onChange })
// Act - Open dropdown (dropdown can still open when disabled)
- const trigger = screen.getByText(/chatAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/chatAssistant.name/i)
+ await user.click(trigger)
// Wait for dropdown to open
await waitFor(() => {
@@ -298,8 +272,8 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'agent', disabled: true })
// Act - Open dropdown
- const trigger = screen.getByText(/agentAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/agentAssistant.name/i)
+ await user.click(trigger)
// Assert - Agent settings option should not be visible
await waitFor(() => {
@@ -313,8 +287,8 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'agent', disabled: false })
// Act - Open dropdown
- const trigger = screen.getByText(/agentAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/agentAssistant.name/i)
+ await user.click(trigger)
// Assert - Agent settings option should be visible
await waitFor(() => {
@@ -331,20 +305,20 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'agent', disabled: false })
// Act - Open dropdown
- const trigger = screen.getByText(/agentAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/agentAssistant.name/i)
+ await user.click(trigger)
// Click agent settings
await waitFor(() => {
expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument()
})
- const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div')
- await user.click(agentSettingsTrigger!)
+ const agentSettingsTrigger = screen.getByText(/agent.setting.name/i)
+ await user.click(agentSettingsTrigger)
// Assert
await waitFor(() => {
- expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument()
+ expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument()
})
})
@@ -354,8 +328,8 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'chat', disabled: false })
// Act - Open dropdown
- const trigger = screen.getByText(/chatAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/chatAssistant.name/i)
+ await user.click(trigger)
// Wait for dropdown to open
await waitFor(() => {
@@ -363,7 +337,7 @@ describe('AssistantTypePicker', () => {
})
// Assert - Agent settings modal should not appear (value is 'chat')
- expect(screen.queryByTestId('agent-setting-modal')).not.toBeInTheDocument()
+ expect(screen.queryByText(/common.operation.save/i)).not.toBeInTheDocument()
})
it('should call onAgentSettingChange when saving agent settings', async () => {
@@ -373,26 +347,26 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'agent', disabled: false, onAgentSettingChange })
// Act - Open dropdown and agent settings
- const trigger = screen.getByText(/agentAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/agentAssistant.name/i)
+ await user.click(trigger)
await waitFor(() => {
expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument()
})
- const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div')
- await user.click(agentSettingsTrigger!)
+ const agentSettingsTrigger = screen.getByText(/agent.setting.name/i)
+ await user.click(agentSettingsTrigger)
// Wait for modal and click save
await waitFor(() => {
- expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument()
+ expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument()
})
- const saveButton = screen.getByText('Save')
+ const saveButton = screen.getByText(/common.operation.save/i)
await user.click(saveButton)
// Assert
- expect(onAgentSettingChange).toHaveBeenCalledWith({ max_iteration: 5 })
+ expect(onAgentSettingChange).toHaveBeenCalledWith(defaultAgentConfig)
})
it('should close modal when saving agent settings', async () => {
@@ -401,26 +375,26 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'agent', disabled: false })
// Act - Open dropdown, agent settings, and save
- const trigger = screen.getByText(/agentAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/agentAssistant.name/i)
+ await user.click(trigger)
await waitFor(() => {
expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument()
})
- const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div')
- await user.click(agentSettingsTrigger!)
+ const agentSettingsTrigger = screen.getByText(/agent.setting.name/i)
+ await user.click(agentSettingsTrigger)
await waitFor(() => {
- expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument()
+ expect(screen.getByText(/appDebug.agent.setting.name/i)).toBeInTheDocument()
})
- const saveButton = screen.getByText('Save')
+ const saveButton = screen.getByText(/common.operation.save/i)
await user.click(saveButton)
// Assert
await waitFor(() => {
- expect(screen.queryByTestId('agent-setting-modal')).not.toBeInTheDocument()
+ expect(screen.queryByText(/common.operation.save/i)).not.toBeInTheDocument()
})
})
@@ -431,26 +405,26 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'agent', disabled: false, onAgentSettingChange })
// Act - Open dropdown, agent settings, and cancel
- const trigger = screen.getByText(/agentAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/agentAssistant.name/i)
+ await user.click(trigger)
await waitFor(() => {
expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument()
})
- const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div')
- await user.click(agentSettingsTrigger!)
+ const agentSettingsTrigger = screen.getByText(/agent.setting.name/i)
+ await user.click(agentSettingsTrigger)
await waitFor(() => {
- expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument()
+ expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument()
})
- const cancelButton = screen.getByText('Cancel')
+ const cancelButton = screen.getByText(/common.operation.cancel/i)
await user.click(cancelButton)
// Assert
await waitFor(() => {
- expect(screen.queryByTestId('agent-setting-modal')).not.toBeInTheDocument()
+ expect(screen.queryByText(/common.operation.save/i)).not.toBeInTheDocument()
})
expect(onAgentSettingChange).not.toHaveBeenCalled()
})
@@ -461,19 +435,19 @@ describe('AssistantTypePicker', () => {
renderComponent({ value: 'agent', disabled: false })
// Act - Open dropdown and agent settings
- const trigger = screen.getByText(/agentAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/agentAssistant.name/i)
+ await user.click(trigger)
await waitFor(() => {
expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument()
})
- const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div')
- await user.click(agentSettingsTrigger!)
+ const agentSettingsTrigger = screen.getByText(/agent.setting.name/i)
+ await user.click(agentSettingsTrigger)
// Assert - Modal should be open and dropdown should close
await waitFor(() => {
- expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument()
+ expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument()
})
// The dropdown should be closed (agent settings description should not be visible)
@@ -492,10 +466,10 @@ describe('AssistantTypePicker', () => {
renderComponent()
// Act
- const trigger = screen.getByText(/chatAssistant.name/i).closest('div')
- await user.click(trigger!)
- await user.click(trigger!)
- await user.click(trigger!)
+ const trigger = screen.getByText(/chatAssistant.name/i)
+ await user.click(trigger)
+ await user.click(trigger)
+ await user.click(trigger)
// Assert - Should not crash
expect(trigger).toBeInTheDocument()
@@ -538,8 +512,8 @@ describe('AssistantTypePicker', () => {
})
}).not.toThrow()
- const trigger = screen.getByText(/chatAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/chatAssistant.name/i)
+ await user.click(trigger)
})
it('should handle empty agentConfig', async () => {
@@ -630,8 +604,8 @@ describe('AssistantTypePicker', () => {
renderComponent()
// Act - Open dropdown
- const trigger = screen.getByText(/chatAssistant.name/i).closest('div')
- await user.click(trigger!)
+ const trigger = screen.getByText(/chatAssistant.name/i)
+ await user.click(trigger)
// Assert - Descriptions should be visible
await waitFor(() => {
@@ -657,18 +631,14 @@ describe('AssistantTypePicker', () => {
})
})
- // Props Validation for AgentSetting
- describe('AgentSetting Props', () => {
- it('should pass isFunctionCall and isChatModel props to AgentSetting', async () => {
+ // Agent Setting Integration
+ describe('AgentSetting Integration', () => {
+ it('should show function call mode when isFunctionCall is true', async () => {
// Arrange
const user = userEvent.setup()
- renderComponent({
- value: 'agent',
- isFunctionCall: true,
- isChatModel: false,
- })
+ renderComponent({ value: 'agent', isFunctionCall: true, isChatModel: false })
- // Act - Open dropdown and trigger AgentSetting
+ // Act - Open dropdown and settings modal
const trigger = screen.getByText(/agentAssistant.name/i)
await user.click(trigger)
@@ -679,17 +649,37 @@ describe('AssistantTypePicker', () => {
const agentSettingsTrigger = screen.getByText(/agent.setting.name/i)
await user.click(agentSettingsTrigger)
- // Assert - Verify AgentSetting receives correct props
+ // Assert
await waitFor(() => {
- expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument()
+ expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument()
})
-
- expect(mockAgentSettingProps).not.toBeNull()
- expect(mockAgentSettingProps!.isFunctionCall).toBe(true)
- expect(mockAgentSettingProps!.isChatModel).toBe(false)
+ expect(screen.getByText(/appDebug.agent.agentModeType.functionCall/i)).toBeInTheDocument()
})
- it('should pass agentConfig payload to AgentSetting', async () => {
+ it('should show built-in prompt when isFunctionCall is false', async () => {
+ // Arrange
+ const user = userEvent.setup()
+ renderComponent({ value: 'agent', isFunctionCall: false, isChatModel: true })
+
+ // Act - Open dropdown and settings modal
+ const trigger = screen.getByText(/agentAssistant.name/i)
+ await user.click(trigger)
+
+ await waitFor(() => {
+ expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument()
+ })
+
+ const agentSettingsTrigger = screen.getByText(/agent.setting.name/i)
+ await user.click(agentSettingsTrigger)
+
+ // Assert
+ await waitFor(() => {
+ expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument()
+ })
+ expect(screen.getByText(/tools.builtInPromptTitle/i)).toBeInTheDocument()
+ })
+
+ it('should initialize max iteration from agentConfig payload', async () => {
// Arrange
const user = userEvent.setup()
const customConfig: AgentConfig = {
@@ -699,12 +689,9 @@ describe('AssistantTypePicker', () => {
tools: [],
}
- renderComponent({
- value: 'agent',
- agentConfig: customConfig,
- })
+ renderComponent({ value: 'agent', agentConfig: customConfig })
- // Act - Open AgentSetting
+ // Act - Open dropdown and settings modal
const trigger = screen.getByText(/agentAssistant.name/i)
await user.click(trigger)
@@ -715,13 +702,10 @@ describe('AssistantTypePicker', () => {
const agentSettingsTrigger = screen.getByText(/agent.setting.name/i)
await user.click(agentSettingsTrigger)
- // Assert - Verify payload was passed
- await waitFor(() => {
- expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument()
- })
-
- expect(mockAgentSettingProps).not.toBeNull()
- expect(mockAgentSettingProps!.payload).toEqual(customConfig)
+ // Assert
+ await screen.findByText(/common.operation.save/i)
+ const maxIterationInput = await screen.findByRole('spinbutton')
+ expect(maxIterationInput).toHaveValue(10)
})
})
diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx
index f76145f901..676456c3ea 100644
--- a/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx
+++ b/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx
@@ -1,5 +1,5 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
-import { createRef } from 'react'
+import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { type ReactNode, type RefObject, createRef } from 'react'
import DebugWithSingleModel from './index'
import type { DebugWithSingleModelRefType } from './index'
import type { ChatItem } from '@/app/components/base/chat/types'
@@ -8,7 +8,8 @@ import type { ProviderContextState } from '@/context/provider-context'
import type { DatasetConfigs, ModelConfig } from '@/models/debug'
import { PromptMode } from '@/models/debug'
import { type Collection, CollectionType } from '@/app/components/tools/types'
-import { AgentStrategy, AppModeEnum, ModelModeType } from '@/types/app'
+import type { FileEntity } from '@/app/components/base/file-uploader/types'
+import { AgentStrategy, AppModeEnum, ModelModeType, Resolution, TransferMethod } from '@/types/app'
// ============================================================================
// Test Data Factories (Following testing.md guidelines)
@@ -67,21 +68,6 @@ function createMockModelConfig(overrides: Partial = {}): ModelConfi
}
}
-/**
- * Factory function for creating mock ChatItem list
- * Note: Currently unused but kept for potential future test cases
- */
-// eslint-disable-next-line unused-imports/no-unused-vars
-function createMockChatList(items: Partial[] = []): ChatItem[] {
- return items.map((item, index) => ({
- id: `msg-${index}`,
- content: 'Test message',
- isAnswer: false,
- message_files: [],
- ...item,
- }))
-}
-
/**
* Factory function for creating mock Collection list
*/
@@ -156,9 +142,9 @@ const mockFetchSuggestedQuestions = jest.fn()
const mockStopChatMessageResponding = jest.fn()
jest.mock('@/service/debug', () => ({
- fetchConversationMessages: (...args: any[]) => mockFetchConversationMessages(...args),
- fetchSuggestedQuestions: (...args: any[]) => mockFetchSuggestedQuestions(...args),
- stopChatMessageResponding: (...args: any[]) => mockStopChatMessageResponding(...args),
+ fetchConversationMessages: (...args: unknown[]) => mockFetchConversationMessages(...args),
+ fetchSuggestedQuestions: (...args: unknown[]) => mockFetchSuggestedQuestions(...args),
+ stopChatMessageResponding: (...args: unknown[]) => mockStopChatMessageResponding(...args),
}))
jest.mock('next/navigation', () => ({
@@ -255,11 +241,11 @@ const mockDebugConfigContext = {
score_threshold: 0.7,
datasets: { datasets: [] },
} as DatasetConfigs,
- datasetConfigsRef: { current: null } as any,
+ datasetConfigsRef: createRef(),
setDatasetConfigs: jest.fn(),
hasSetContextVar: false,
isShowVisionConfig: false,
- visionConfig: { enabled: false, number_limits: 2, detail: 'low' as any, transfer_methods: [] },
+ visionConfig: { enabled: false, number_limits: 2, detail: Resolution.low, transfer_methods: [] },
setVisionConfig: jest.fn(),
isAllowVideoUpload: false,
isShowDocumentConfig: false,
@@ -295,7 +281,19 @@ jest.mock('@/context/app-context', () => ({
useAppContext: jest.fn(() => mockAppContext),
}))
-const mockFeatures = {
+type FeatureState = {
+ moreLikeThis: { enabled: boolean }
+ opening: { enabled: boolean; opening_statement: string; suggested_questions: string[] }
+ moderation: { enabled: boolean }
+ speech2text: { enabled: boolean }
+ text2speech: { enabled: boolean }
+ file: { enabled: boolean }
+ suggested: { enabled: boolean }
+ citation: { enabled: boolean }
+ annotationReply: { enabled: boolean }
+}
+
+const defaultFeatures: FeatureState = {
moreLikeThis: { enabled: false },
opening: { enabled: false, opening_statement: '', suggested_questions: [] },
moderation: { enabled: false },
@@ -306,13 +304,11 @@ const mockFeatures = {
citation: { enabled: false },
annotationReply: { enabled: false },
}
+type FeatureSelector = (state: { features: FeatureState }) => unknown
+let mockFeaturesState: FeatureState = { ...defaultFeatures }
jest.mock('@/app/components/base/features/hooks', () => ({
- useFeatures: jest.fn((selector) => {
- if (typeof selector === 'function')
- return selector({ features: mockFeatures })
- return mockFeatures
- }),
+ useFeatures: jest.fn(),
}))
const mockConfigFromDebugContext = {
@@ -345,7 +341,7 @@ jest.mock('../hooks', () => ({
const mockSetShowAppConfigureFeaturesModal = jest.fn()
jest.mock('@/app/components/app/store', () => ({
- useStore: jest.fn((selector) => {
+ useStore: jest.fn((selector?: (state: { setShowAppConfigureFeaturesModal: typeof mockSetShowAppConfigureFeaturesModal }) => unknown) => {
if (typeof selector === 'function')
return selector({ setShowAppConfigureFeaturesModal: mockSetShowAppConfigureFeaturesModal })
return mockSetShowAppConfigureFeaturesModal
@@ -384,12 +380,31 @@ jest.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({
},
}))
-// Mock external APIs that might be used
-globalThis.ResizeObserver = jest.fn().mockImplementation(() => ({
- observe: jest.fn(),
- unobserve: jest.fn(),
- disconnect: jest.fn(),
-}))
+type MockChatProps = {
+ chatList?: ChatItem[]
+ isResponding?: boolean
+ onSend?: (message: string, files?: FileEntity[]) => void
+ onRegenerate?: (chatItem: ChatItem, editedQuestion?: { message: string; files?: FileEntity[] }) => void
+ onStopResponding?: () => void
+ suggestedQuestions?: string[]
+ questionIcon?: ReactNode
+ answerIcon?: ReactNode
+ onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
+ onAnnotationEdited?: (question: string, answer: string, index: number) => void
+ onAnnotationRemoved?: (index: number) => void
+ switchSibling?: (siblingMessageId: string) => void
+ onFeatureBarClick?: (state: boolean) => void
+}
+
+const mockFile: FileEntity = {
+ id: 'file-1',
+ name: 'test.png',
+ size: 123,
+ type: 'image/png',
+ progress: 100,
+ transferMethod: TransferMethod.local_file,
+ supportFileType: 'image',
+}
// Mock Chat component (complex with many dependencies)
// This is a pragmatic mock that tests the integration at DebugWithSingleModel level
@@ -408,11 +423,13 @@ jest.mock('@/app/components/base/chat/chat', () => {
onAnnotationRemoved,
switchSibling,
onFeatureBarClick,
- }: any) {
+ }: MockChatProps) {
+ const items = chatList || []
+ const suggested = suggestedQuestions ?? []
return (
- {chatList?.map((item: any) => (
+ {items.map((item: ChatItem) => (
{item.content}
@@ -434,14 +451,21 @@ jest.mock('@/app/components/base/chat/chat', () => {
>
Send
+
{isResponding && (
)}
- {suggestedQuestions?.length > 0 && (
+ {suggested.length > 0 && (
- {suggestedQuestions.map((q: string, i: number) => (
+ {suggested.map((q: string, i: number) => (
@@ -451,7 +475,13 @@ jest.mock('@/app/components/base/chat/chat', () => {
{onRegenerate && (
@@ -506,12 +536,30 @@ jest.mock('@/app/components/base/chat/chat', () => {
// ============================================================================
describe('DebugWithSingleModel', () => {
- let ref: React.RefObject
+ let ref: RefObject
beforeEach(() => {
jest.clearAllMocks()
ref = createRef()
+ const { useDebugConfigurationContext } = require('@/context/debug-configuration')
+ const { useProviderContext } = require('@/context/provider-context')
+ const { useAppContext } = require('@/context/app-context')
+ const { useConfigFromDebugContext, useFormattingChangedSubscription } = require('../hooks')
+ const { useFeatures } = require('@/app/components/base/features/hooks') as { useFeatures: jest.Mock }
+
+ useDebugConfigurationContext.mockReturnValue(mockDebugConfigContext)
+ useProviderContext.mockReturnValue(mockProviderContext)
+ useAppContext.mockReturnValue(mockAppContext)
+ useConfigFromDebugContext.mockReturnValue(mockConfigFromDebugContext)
+ useFormattingChangedSubscription.mockReturnValue(undefined)
+ mockFeaturesState = { ...defaultFeatures }
+ useFeatures.mockImplementation((selector?: FeatureSelector) => {
+ if (typeof selector === 'function')
+ return selector({ features: mockFeaturesState })
+ return mockFeaturesState
+ })
+
// Reset mock implementations
mockFetchConversationMessages.mockResolvedValue({ data: [] })
mockFetchSuggestedQuestions.mockResolvedValue({ data: [] })
@@ -521,7 +569,7 @@ describe('DebugWithSingleModel', () => {
// Rendering Tests
describe('Rendering', () => {
it('should render without crashing', () => {
- render(} />)
+ render(} />)
// Verify Chat component is rendered
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
@@ -532,7 +580,7 @@ describe('DebugWithSingleModel', () => {
it('should render with custom checkCanSend prop', () => {
const checkCanSend = jest.fn(() => true)
- render(} checkCanSend={checkCanSend} />)
+ render(} checkCanSend={checkCanSend} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -543,122 +591,88 @@ describe('DebugWithSingleModel', () => {
it('should respect checkCanSend returning true', async () => {
const checkCanSend = jest.fn(() => true)
- render(} checkCanSend={checkCanSend} />)
+ render(} checkCanSend={checkCanSend} />)
const sendButton = screen.getByTestId('send-button')
fireEvent.click(sendButton)
+ const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
await waitFor(() => {
expect(checkCanSend).toHaveBeenCalled()
+ expect(ssePost).toHaveBeenCalled()
})
+
+ expect(ssePost.mock.calls[0][0]).toBe('apps/test-app-id/chat-messages')
})
it('should prevent send when checkCanSend returns false', async () => {
const checkCanSend = jest.fn(() => false)
- render(} checkCanSend={checkCanSend} />)
+ render(} checkCanSend={checkCanSend} />)
const sendButton = screen.getByTestId('send-button')
fireEvent.click(sendButton)
+ const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
await waitFor(() => {
expect(checkCanSend).toHaveBeenCalled()
expect(checkCanSend).toHaveReturnedWith(false)
})
+ expect(ssePost).not.toHaveBeenCalled()
})
})
- // Context Integration Tests
- describe('Context Integration', () => {
- it('should use debug configuration context', () => {
- const { useDebugConfigurationContext } = require('@/context/debug-configuration')
+ // User Interactions
+ describe('User Interactions', () => {
+ it('should open feature configuration when feature bar is clicked', () => {
+ render(} />)
- render(} />)
+ fireEvent.click(screen.getByTestId('feature-bar-button'))
- expect(useDebugConfigurationContext).toHaveBeenCalled()
- })
-
- it('should use provider context for model list', () => {
- const { useProviderContext } = require('@/context/provider-context')
-
- render(} />)
-
- expect(useProviderContext).toHaveBeenCalled()
- })
-
- it('should use app context for user profile', () => {
- const { useAppContext } = require('@/context/app-context')
-
- render(} />)
-
- expect(useAppContext).toHaveBeenCalled()
- })
-
- it('should use features from features hook', () => {
- const { useFeatures } = require('@/app/components/base/features/hooks')
-
- render(} />)
-
- expect(useFeatures).toHaveBeenCalled()
- })
-
- it('should use config from debug context hook', () => {
- const { useConfigFromDebugContext } = require('../hooks')
-
- render(} />)
-
- expect(useConfigFromDebugContext).toHaveBeenCalled()
- })
-
- it('should subscribe to formatting changes', () => {
- const { useFormattingChangedSubscription } = require('../hooks')
-
- render(} />)
-
- expect(useFormattingChangedSubscription).toHaveBeenCalled()
+ expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalledWith(true)
})
})
// Model Configuration Tests
describe('Model Configuration', () => {
- it('should merge features into config correctly when all features enabled', () => {
- const { useFeatures } = require('@/app/components/base/features/hooks')
+ it('should include opening features in request when enabled', async () => {
+ mockFeaturesState = {
+ ...defaultFeatures,
+ opening: { enabled: true, opening_statement: 'Hello!', suggested_questions: ['Q1'] },
+ }
- useFeatures.mockReturnValue((selector: any) => {
- const features = {
- moreLikeThis: { enabled: true },
- opening: { enabled: true, opening_statement: 'Hello!', suggested_questions: ['Q1'] },
- moderation: { enabled: true },
- speech2text: { enabled: true },
- text2speech: { enabled: true },
- file: { enabled: true },
- suggested: { enabled: true },
- citation: { enabled: true },
- annotationReply: { enabled: true },
- }
- return typeof selector === 'function' ? selector({ features }) : features
+ render(} />)
+
+ fireEvent.click(screen.getByTestId('send-button'))
+
+ const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
+ await waitFor(() => {
+ expect(ssePost).toHaveBeenCalled()
})
- render(} />)
-
- expect(screen.getByTestId('chat-component')).toBeInTheDocument()
+ const body = ssePost.mock.calls[0][1].body
+ expect(body.model_config.opening_statement).toBe('Hello!')
+ expect(body.model_config.suggested_questions).toEqual(['Q1'])
})
- it('should handle opening feature disabled correctly', () => {
- const { useFeatures } = require('@/app/components/base/features/hooks')
+ it('should omit opening statement when feature is disabled', async () => {
+ mockFeaturesState = {
+ ...defaultFeatures,
+ opening: { enabled: false, opening_statement: 'Should not appear', suggested_questions: ['Q1'] },
+ }
- useFeatures.mockReturnValue((selector: any) => {
- const features = {
- ...mockFeatures,
- opening: { enabled: false, opening_statement: 'Should not appear', suggested_questions: ['Q1'] },
- }
- return typeof selector === 'function' ? selector({ features }) : features
+ render(} />)
+
+ fireEvent.click(screen.getByTestId('send-button'))
+
+ const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
+ await waitFor(() => {
+ expect(ssePost).toHaveBeenCalled()
})
- render(} />)
-
- // When opening is disabled, opening_statement should be empty
- expect(screen.queryByText('Should not appear')).not.toBeInTheDocument()
+ const body = ssePost.mock.calls[0][1].body
+ expect(body.model_config.opening_statement).toBe('')
+ expect(body.model_config.suggested_questions).toEqual([])
})
it('should handle model without vision support', () => {
@@ -689,7 +703,7 @@ describe('DebugWithSingleModel', () => {
],
}))
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -710,7 +724,7 @@ describe('DebugWithSingleModel', () => {
],
}))
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -735,7 +749,7 @@ describe('DebugWithSingleModel', () => {
}),
})
- render(} />)
+ render(} />)
// Component should render successfully with filtered variables
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
@@ -754,7 +768,7 @@ describe('DebugWithSingleModel', () => {
}),
})
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -763,7 +777,7 @@ describe('DebugWithSingleModel', () => {
// Tool Icons Tests
describe('Tool Icons', () => {
it('should map tool icons from collection list', () => {
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -783,7 +797,7 @@ describe('DebugWithSingleModel', () => {
}),
})
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -812,7 +826,7 @@ describe('DebugWithSingleModel', () => {
collectionList: [],
})
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -828,7 +842,7 @@ describe('DebugWithSingleModel', () => {
inputs: {},
})
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -846,7 +860,7 @@ describe('DebugWithSingleModel', () => {
},
})
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -859,7 +873,7 @@ describe('DebugWithSingleModel', () => {
completionParams: {},
})
- render(} />)
+ render(} />)
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
@@ -868,7 +882,7 @@ describe('DebugWithSingleModel', () => {
// Imperative Handle Tests
describe('Imperative Handle', () => {
it('should expose handleRestart method via ref', () => {
- render(} />)
+ render(} />)
expect(ref.current).not.toBeNull()
expect(ref.current?.handleRestart).toBeDefined()
@@ -876,65 +890,26 @@ describe('DebugWithSingleModel', () => {
})
it('should call handleRestart when invoked via ref', () => {
- render(} />)
+ render(} />)
- expect(() => {
+ act(() => {
ref.current?.handleRestart()
- }).not.toThrow()
- })
- })
-
- // Memory and Performance Tests
- describe('Memory and Performance', () => {
- it('should properly memoize component', () => {
- const { rerender } = render(} />)
-
- // Re-render with same props
- rerender(} />)
-
- expect(screen.getByTestId('chat-component')).toBeInTheDocument()
- })
-
- it('should have displayName set for debugging', () => {
- expect(DebugWithSingleModel).toBeDefined()
- // memo wraps the component
- expect(typeof DebugWithSingleModel).toBe('object')
- })
- })
-
- // Async Operations Tests
- describe('Async Operations', () => {
- it('should handle API calls during message send', async () => {
- mockFetchConversationMessages.mockResolvedValue({ data: [] })
-
- render(} />)
-
- const textarea = screen.getByRole('textbox', { hidden: true })
- fireEvent.change(textarea, { target: { value: 'Test message' } })
-
- // Component should render without errors during async operations
- await waitFor(() => {
- expect(screen.getByTestId('chat-component')).toBeInTheDocument()
- })
- })
-
- it('should handle API errors gracefully', async () => {
- mockFetchConversationMessages.mockRejectedValue(new Error('API Error'))
-
- render(} />)
-
- // Component should still render even if API calls fail
- await waitFor(() => {
- expect(screen.getByTestId('chat-component')).toBeInTheDocument()
})
})
})
// File Upload Tests
describe('File Upload', () => {
- it('should not include files when vision is not supported', () => {
+ it('should not include files when vision is not supported', async () => {
+ const { useDebugConfigurationContext } = require('@/context/debug-configuration')
const { useProviderContext } = require('@/context/provider-context')
- const { useFeatures } = require('@/app/components/base/features/hooks')
+
+ useDebugConfigurationContext.mockReturnValue({
+ ...mockDebugConfigContext,
+ modelConfig: createMockModelConfig({
+ model_id: 'gpt-3.5-turbo',
+ }),
+ })
useProviderContext.mockReturnValue(createMockProviderContext({
textGenerationModelList: [
@@ -961,23 +936,34 @@ describe('DebugWithSingleModel', () => {
],
}))
- useFeatures.mockReturnValue((selector: any) => {
- const features = {
- ...mockFeatures,
- file: { enabled: true }, // File upload enabled
- }
- return typeof selector === 'function' ? selector({ features }) : features
+ mockFeaturesState = {
+ ...defaultFeatures,
+ file: { enabled: true },
+ }
+
+ render(} />)
+
+ fireEvent.click(screen.getByTestId('send-with-files'))
+
+ const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
+ await waitFor(() => {
+ expect(ssePost).toHaveBeenCalled()
})
- render(} />)
-
- // Should render but not allow file uploads
- expect(screen.getByTestId('chat-component')).toBeInTheDocument()
+ const body = ssePost.mock.calls[0][1].body
+ expect(body.files).toEqual([])
})
- it('should support files when vision is enabled', () => {
+ it('should support files when vision is enabled', async () => {
+ const { useDebugConfigurationContext } = require('@/context/debug-configuration')
const { useProviderContext } = require('@/context/provider-context')
- const { useFeatures } = require('@/app/components/base/features/hooks')
+
+ useDebugConfigurationContext.mockReturnValue({
+ ...mockDebugConfigContext,
+ modelConfig: createMockModelConfig({
+ model_id: 'gpt-4-vision',
+ }),
+ })
useProviderContext.mockReturnValue(createMockProviderContext({
textGenerationModelList: [
@@ -1004,17 +990,22 @@ describe('DebugWithSingleModel', () => {
],
}))
- useFeatures.mockReturnValue((selector: any) => {
- const features = {
- ...mockFeatures,
- file: { enabled: true },
- }
- return typeof selector === 'function' ? selector({ features }) : features
+ mockFeaturesState = {
+ ...defaultFeatures,
+ file: { enabled: true },
+ }
+
+ render(} />)
+
+ fireEvent.click(screen.getByTestId('send-with-files'))
+
+ const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
+ await waitFor(() => {
+ expect(ssePost).toHaveBeenCalled()
})
- render(} />)
-
- expect(screen.getByTestId('chat-component')).toBeInTheDocument()
+ const body = ssePost.mock.calls[0][1].body
+ expect(body.files).toHaveLength(1)
})
})
})
diff --git a/web/app/components/billing/upgrade-btn/index.spec.tsx b/web/app/components/billing/upgrade-btn/index.spec.tsx
index f52cc97b01..d106dbe327 100644
--- a/web/app/components/billing/upgrade-btn/index.spec.tsx
+++ b/web/app/components/billing/upgrade-btn/index.spec.tsx
@@ -5,24 +5,6 @@ import UpgradeBtn from './index'
// ✅ Import real project components (DO NOT mock these)
// PremiumBadge, Button, SparklesSoft are all base components
-// ✅ Mock i18n with actual translations instead of returning keys
-jest.mock('react-i18next', () => ({
- useTranslation: () => ({
- t: (key: string) => {
- const translations: Record = {
- 'billing.upgradeBtn.encourage': 'Upgrade to Pro',
- 'billing.upgradeBtn.encourageShort': 'Upgrade',
- 'billing.upgradeBtn.plain': 'Upgrade Plan',
- 'custom.label.key': 'Custom Label',
- 'custom.key': 'Custom Text',
- 'custom.short.key': 'Short Custom',
- 'custom.all': 'All Custom Props',
- }
- return translations[key] || key
- },
- }),
-}))
-
// ✅ Mock external dependencies only
const mockSetShowPricingModal = jest.fn()
jest.mock('@/context/modal-context', () => ({
@@ -52,7 +34,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - should render with default text
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
it('should render premium badge by default', () => {
@@ -60,7 +42,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - PremiumBadge renders with text content
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
it('should render plain button when isPlain is true', () => {
@@ -70,7 +52,7 @@ describe('UpgradeBtn', () => {
// Assert - Button should be rendered with plain text
const button = screen.getByRole('button')
expect(button).toBeInTheDocument()
- expect(screen.getByText(/upgrade plan/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.plain/i)).toBeInTheDocument()
})
it('should render short text when isShort is true', () => {
@@ -78,7 +60,7 @@ describe('UpgradeBtn', () => {
render()
// Assert
- expect(screen.getByText(/^upgrade$/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourageShort/i)).toBeInTheDocument()
})
it('should render custom label when labelKey is provided', () => {
@@ -86,7 +68,7 @@ describe('UpgradeBtn', () => {
render()
// Assert
- expect(screen.getByText(/custom label/i)).toBeInTheDocument()
+ expect(screen.getByText(/custom\.label\.key/i)).toBeInTheDocument()
})
it('should render custom label in plain button when labelKey is provided with isPlain', () => {
@@ -96,7 +78,7 @@ describe('UpgradeBtn', () => {
// Assert
const button = screen.getByRole('button')
expect(button).toBeInTheDocument()
- expect(screen.getByText(/custom label/i)).toBeInTheDocument()
+ expect(screen.getByText(/custom\.label\.key/i)).toBeInTheDocument()
})
})
@@ -155,7 +137,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - Component renders successfully with size prop
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
it('should render with size "m" by default', () => {
@@ -163,7 +145,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - Component renders successfully
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
it('should render with size "custom"', () => {
@@ -171,7 +153,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - Component renders successfully with custom size
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
})
@@ -184,8 +166,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert
expect(handleClick).toHaveBeenCalledTimes(1)
@@ -213,8 +195,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
@@ -240,8 +222,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert
expect(mockGtag).toHaveBeenCalledTimes(1)
@@ -273,8 +255,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert
expect(mockGtag).not.toHaveBeenCalled()
@@ -287,8 +269,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert - should not throw error
expect(mockGtag).not.toHaveBeenCalled()
@@ -302,8 +284,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert
expect(handleClick).toHaveBeenCalledTimes(1)
@@ -321,7 +303,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - should render without error
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
it('should handle undefined style', () => {
@@ -329,7 +311,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - should render without error
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
it('should handle undefined onClick', async () => {
@@ -338,8 +320,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert - should fall back to setShowPricingModal
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
@@ -351,8 +333,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert - should not attempt to track gtag
expect(mockGtag).not.toHaveBeenCalled()
@@ -363,7 +345,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - should use default label
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
it('should handle empty string className', () => {
@@ -371,7 +353,7 @@ describe('UpgradeBtn', () => {
render()
// Assert
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
it('should handle empty string loc', async () => {
@@ -380,8 +362,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert - empty loc should not trigger gtag
expect(mockGtag).not.toHaveBeenCalled()
@@ -392,7 +374,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - empty labelKey is falsy, so it falls back to default label
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
})
@@ -403,7 +385,7 @@ describe('UpgradeBtn', () => {
render()
// Assert - isShort should not affect plain button text
- expect(screen.getByText(/upgrade plan/i)).toBeInTheDocument()
+ expect(screen.getByText(/billing\.upgradeBtn\.plain/i)).toBeInTheDocument()
})
it('should handle isPlain with custom labelKey', () => {
@@ -411,8 +393,8 @@ describe('UpgradeBtn', () => {
render()
// Assert - labelKey should override plain text
- expect(screen.getByText(/custom text/i)).toBeInTheDocument()
- expect(screen.queryByText(/upgrade plan/i)).not.toBeInTheDocument()
+ expect(screen.getByText(/custom\.key/i)).toBeInTheDocument()
+ expect(screen.queryByText(/billing\.upgradeBtn\.plain/i)).not.toBeInTheDocument()
})
it('should handle isShort with custom labelKey', () => {
@@ -420,8 +402,8 @@ describe('UpgradeBtn', () => {
render()
// Assert - labelKey should override isShort behavior
- expect(screen.getByText(/short custom/i)).toBeInTheDocument()
- expect(screen.queryByText(/^upgrade$/i)).not.toBeInTheDocument()
+ expect(screen.getByText(/custom\.short\.key/i)).toBeInTheDocument()
+ expect(screen.queryByText(/billing\.upgradeBtn\.encourageShort/i)).not.toBeInTheDocument()
})
it('should handle all custom props together', async () => {
@@ -443,14 +425,14 @@ describe('UpgradeBtn', () => {
labelKey="custom.all"
/>,
)
- const badge = screen.getByText(/all custom props/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/custom\.all/i)
+ await user.click(badge)
// Assert
const rootElement = container.firstChild as HTMLElement
expect(rootElement).toHaveClass(customClass)
expect(rootElement).toHaveStyle(customStyle)
- expect(screen.getByText(/all custom props/i)).toBeInTheDocument()
+ expect(screen.getByText(/custom\.all/i)).toBeInTheDocument()
expect(handleClick).toHaveBeenCalledTimes(1)
expect(mockGtag).toHaveBeenCalledWith('event', 'click_upgrade_btn', {
loc: 'test-loc',
@@ -503,10 +485,10 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
// Click badge
- await user.click(badge!)
+ await user.click(badge)
// Assert
expect(handleClick).toHaveBeenCalledTimes(1)
@@ -522,70 +504,6 @@ describe('UpgradeBtn', () => {
})
})
- // Performance Tests
- describe('Performance', () => {
- it('should not rerender when props do not change', () => {
- // Arrange
- const { rerender } = render()
- const firstRender = screen.getByText(/upgrade to pro/i)
-
- // Act - Rerender with same props
- rerender()
-
- // Assert - Component should still be in document
- expect(firstRender).toBeInTheDocument()
- expect(screen.getByText(/upgrade to pro/i)).toBe(firstRender)
- })
-
- it('should rerender when props change', () => {
- // Arrange
- const { rerender } = render()
- expect(screen.getByText(/custom text/i)).toBeInTheDocument()
-
- // Act - Rerender with different labelKey
- rerender()
-
- // Assert - Should show new label
- expect(screen.getByText(/custom label/i)).toBeInTheDocument()
- expect(screen.queryByText(/custom text/i)).not.toBeInTheDocument()
- })
-
- it('should handle rapid rerenders efficiently', () => {
- // Arrange
- const { rerender } = render()
-
- // Act - Multiple rapid rerenders
- for (let i = 0; i < 10; i++)
- rerender()
-
- // Assert - Component should still render correctly
- expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument()
- })
-
- it('should be memoized with React.memo', () => {
- // Arrange
- const TestWrapper = ({ children }: { children: React.ReactNode }) => {children}
-
- const { rerender } = render(
-
-
- ,
- )
-
- const firstElement = screen.getByText(/upgrade to pro/i)
-
- // Act - Rerender parent with same props
- rerender(
-
-
- ,
- )
-
- // Assert - Element reference should be stable due to memo
- expect(screen.getByText(/upgrade to pro/i)).toBe(firstElement)
- })
- })
-
// Integration Tests
describe('Integration', () => {
it('should work with modal context for pricing modal', async () => {
@@ -594,8 +512,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert
await waitFor(() => {
@@ -610,8 +528,8 @@ describe('UpgradeBtn', () => {
// Act
render()
- const badge = screen.getByText(/upgrade to pro/i).closest('div')
- await user.click(badge!)
+ const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
+ await user.click(badge)
// Assert - Both onClick and gtag should be called
await waitFor(() => {
diff --git a/web/app/components/explore/installed-app/index.spec.tsx b/web/app/components/explore/installed-app/index.spec.tsx
index 61ef575183..7dbf31aa42 100644
--- a/web/app/components/explore/installed-app/index.spec.tsx
+++ b/web/app/components/explore/installed-app/index.spec.tsx
@@ -172,7 +172,7 @@ describe('InstalledApp', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render()
- expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
+ expect(screen.getByText(/Chat With History/i)).toBeInTheDocument()
})
it('should render loading state when fetching app params', () => {
@@ -296,8 +296,8 @@ describe('InstalledApp', () => {
describe('App Mode Rendering', () => {
it('should render ChatWithHistory for CHAT mode', () => {
render()
- expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
- expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument()
+ expect(screen.getByText(/Chat With History/i)).toBeInTheDocument()
+ expect(screen.queryByText(/Text Generation App/i)).not.toBeInTheDocument()
})
it('should render ChatWithHistory for ADVANCED_CHAT mode', () => {
@@ -314,8 +314,8 @@ describe('InstalledApp', () => {
})
render()
- expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
- expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument()
+ expect(screen.getByText(/Chat With History/i)).toBeInTheDocument()
+ expect(screen.queryByText(/Text Generation App/i)).not.toBeInTheDocument()
})
it('should render ChatWithHistory for AGENT_CHAT mode', () => {
@@ -332,8 +332,8 @@ describe('InstalledApp', () => {
})
render()
- expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
- expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument()
+ expect(screen.getByText(/Chat With History/i)).toBeInTheDocument()
+ expect(screen.queryByText(/Text Generation App/i)).not.toBeInTheDocument()
})
it('should render TextGenerationApp for COMPLETION mode', () => {
@@ -350,8 +350,7 @@ describe('InstalledApp', () => {
})
render()
- expect(screen.getByTestId('text-generation-app')).toBeInTheDocument()
- expect(screen.getByText(/Text Generation App/)).toBeInTheDocument()
+ expect(screen.getByText(/Text Generation App/i)).toBeInTheDocument()
expect(screen.queryByText(/Workflow/)).not.toBeInTheDocument()
})
@@ -369,7 +368,7 @@ describe('InstalledApp', () => {
})
render()
- expect(screen.getByTestId('text-generation-app')).toBeInTheDocument()
+ expect(screen.getByText(/Text Generation App/i)).toBeInTheDocument()
expect(screen.getByText(/Workflow/)).toBeInTheDocument()
})
})
@@ -566,22 +565,10 @@ describe('InstalledApp', () => {
render()
// Should find and render the correct app
- expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
+ expect(screen.getByText(/Chat With History/i)).toBeInTheDocument()
expect(screen.getByText(/installed-app-123/)).toBeInTheDocument()
})
- it('should apply correct CSS classes to container', () => {
- const { container } = render()
- const mainDiv = container.firstChild as HTMLElement
- expect(mainDiv).toHaveClass('h-full', 'bg-background-default', 'py-2', 'pl-0', 'pr-2', 'sm:p-2')
- })
-
- it('should apply correct CSS classes to ChatWithHistory', () => {
- render()
- const chatComponent = screen.getByTestId('chat-with-history')
- expect(chatComponent).toHaveClass('overflow-hidden', 'rounded-2xl', 'shadow-md')
- })
-
it('should handle rapid id prop changes', async () => {
const app1 = { ...mockInstalledApp, id: 'app-1' }
const app2 = { ...mockInstalledApp, id: 'app-2' }
@@ -627,50 +614,6 @@ describe('InstalledApp', () => {
})
})
- describe('Component Memoization', () => {
- it('should be wrapped with React.memo', () => {
- // React.memo wraps the component with a special $$typeof symbol
- const componentType = (InstalledApp as React.MemoExoticComponent).$$typeof
- expect(componentType).toBeDefined()
- })
-
- it('should re-render when props change', () => {
- const { rerender } = render()
- expect(screen.getByText(/installed-app-123/)).toBeInTheDocument()
-
- // Change to a different app
- const differentApp = {
- ...mockInstalledApp,
- id: 'different-app-456',
- app: {
- ...mockInstalledApp.app,
- name: 'Different App',
- },
- }
- ;(useContext as jest.Mock).mockReturnValue({
- installedApps: [differentApp],
- isFetchingInstalledApps: false,
- })
-
- rerender()
- expect(screen.getByText(/different-app-456/)).toBeInTheDocument()
- })
-
- it('should maintain component stability across re-renders with same props', () => {
- const { rerender } = render()
- const initialCallCount = mockUpdateAppInfo.mock.calls.length
-
- // Rerender with same props - useEffect may still run due to dependencies
- rerender()
-
- // Component should render successfully
- expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
-
- // Mock calls might increase due to useEffect, but component should be stable
- expect(mockUpdateAppInfo.mock.calls.length).toBeGreaterThanOrEqual(initialCallCount)
- })
- })
-
describe('Render Priority', () => {
it('should show error before loading state', () => {
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
diff --git a/web/jest.config.ts b/web/jest.config.ts
index 6c2d88448c..e86ec5af74 100644
--- a/web/jest.config.ts
+++ b/web/jest.config.ts
@@ -44,6 +44,7 @@ const config: Config = {
// A list of reporter names that Jest uses when writing coverage reports
coverageReporters: [
+ 'json-summary',
'json',
'text',
'text-summary',
diff --git a/web/jest.setup.ts b/web/jest.setup.ts
index 9c3b0bf3bd..a4d358d805 100644
--- a/web/jest.setup.ts
+++ b/web/jest.setup.ts
@@ -42,6 +42,22 @@ if (typeof window !== 'undefined') {
ensureWritable(HTMLElement.prototype, 'focus')
}
+if (typeof globalThis.ResizeObserver === 'undefined') {
+ globalThis.ResizeObserver = class {
+ observe() {
+ return undefined
+ }
+
+ unobserve() {
+ return undefined
+ }
+
+ disconnect() {
+ return undefined
+ }
+ }
+}
+
afterEach(() => {
cleanup()
})
diff --git a/web/package.json b/web/package.json
index d54e6effb2..158dfbcae8 100644
--- a/web/package.json
+++ b/web/package.json
@@ -200,6 +200,7 @@
"eslint-plugin-tailwindcss": "^3.18.2",
"globals": "^15.15.0",
"husky": "^9.1.7",
+ "istanbul-lib-coverage": "^3.2.2",
"jest": "^29.7.0",
"jsdom-testing-mocks": "^1.16.0",
"knip": "^5.66.1",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 8523215a07..6dbc0fabd9 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -512,6 +512,9 @@ importers:
husky:
specifier: ^9.1.7
version: 9.1.7
+ istanbul-lib-coverage:
+ specifier: ^3.2.2
+ version: 3.2.2
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.9.3))