mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 09:17:19 -05:00
chore(web): enhance frontend tests (#29869)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
108
.github/workflows/web-tests.yml
vendored
108
.github/workflows/web-tests.yml
vendored
@@ -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('<details><summary>Jest coverage table</summary>');
|
||||
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('</details>');
|
||||
}
|
||||
NODE
|
||||
|
||||
@@ -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 (
|
||||
<div data-testid="agent-setting-modal">
|
||||
<button onClick={() => props.onSave({ max_iteration: 5 } as AgentConfig)}>Save</button>
|
||||
<button onClick={props.onCancel}>Cancel</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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<ModelConfig> = {}): 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>[] = []): 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<DatasetConfigs>(),
|
||||
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 (
|
||||
<div data-testid="chat-component">
|
||||
<div data-testid="chat-list">
|
||||
{chatList?.map((item: any) => (
|
||||
{items.map((item: ChatItem) => (
|
||||
<div key={item.id} data-testid={`chat-item-${item.id}`}>
|
||||
{item.content}
|
||||
</div>
|
||||
@@ -434,14 +451,21 @@ jest.mock('@/app/components/base/chat/chat', () => {
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<button
|
||||
data-testid="send-with-files"
|
||||
onClick={() => onSend?.('test message', [mockFile])}
|
||||
disabled={isResponding}
|
||||
>
|
||||
Send With Files
|
||||
</button>
|
||||
{isResponding && (
|
||||
<button data-testid="stop-button" onClick={onStopResponding}>
|
||||
Stop
|
||||
</button>
|
||||
)}
|
||||
{suggestedQuestions?.length > 0 && (
|
||||
{suggested.length > 0 && (
|
||||
<div data-testid="suggested-questions">
|
||||
{suggestedQuestions.map((q: string, i: number) => (
|
||||
{suggested.map((q: string, i: number) => (
|
||||
<button key={i} onClick={() => onSend?.(q, [])}>
|
||||
{q}
|
||||
</button>
|
||||
@@ -451,7 +475,13 @@ jest.mock('@/app/components/base/chat/chat', () => {
|
||||
{onRegenerate && (
|
||||
<button
|
||||
data-testid="regenerate-button"
|
||||
onClick={() => onRegenerate({ id: 'msg-1', parentMessageId: 'msg-0' })}
|
||||
onClick={() => onRegenerate({
|
||||
id: 'msg-1',
|
||||
content: 'Question',
|
||||
isAnswer: false,
|
||||
message_files: [],
|
||||
parentMessageId: 'msg-0',
|
||||
})}
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
@@ -506,12 +536,30 @@ jest.mock('@/app/components/base/chat/chat', () => {
|
||||
// ============================================================================
|
||||
|
||||
describe('DebugWithSingleModel', () => {
|
||||
let ref: React.RefObject<DebugWithSingleModelRefType | null>
|
||||
let ref: RefObject<DebugWithSingleModelRefType | null>
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
ref = createRef<DebugWithSingleModelRefType | null>()
|
||||
|
||||
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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
// 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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} checkCanSend={checkCanSend} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} 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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} checkCanSend={checkCanSend} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} 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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} checkCanSend={checkCanSend} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} 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(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(useProviderContext).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use app context for user profile', () => {
|
||||
const { useAppContext } = require('@/context/app-context')
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(useAppContext).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use features from features hook', () => {
|
||||
const { useFeatures } = require('@/app/components/base/features/hooks')
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(useFeatures).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use config from debug context hook', () => {
|
||||
const { useConfigFromDebugContext } = require('../hooks')
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(useConfigFromDebugContext).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should subscribe to formatting changes', () => {
|
||||
const { useFormattingChangedSubscription } = require('../hooks')
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
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(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-button'))
|
||||
|
||||
const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
|
||||
await waitFor(() => {
|
||||
expect(ssePost).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
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(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-button'))
|
||||
|
||||
const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
|
||||
await waitFor(() => {
|
||||
expect(ssePost).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
// 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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
})
|
||||
@@ -710,7 +724,7 @@ describe('DebugWithSingleModel', () => {
|
||||
],
|
||||
}))
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
})
|
||||
@@ -735,7 +749,7 @@ describe('DebugWithSingleModel', () => {
|
||||
}),
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
// Component should render successfully with filtered variables
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
@@ -754,7 +768,7 @@ describe('DebugWithSingleModel', () => {
|
||||
}),
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
})
|
||||
@@ -783,7 +797,7 @@ describe('DebugWithSingleModel', () => {
|
||||
}),
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
})
|
||||
@@ -812,7 +826,7 @@ describe('DebugWithSingleModel', () => {
|
||||
collectionList: [],
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
})
|
||||
@@ -828,7 +842,7 @@ describe('DebugWithSingleModel', () => {
|
||||
inputs: {},
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
})
|
||||
@@ -846,7 +860,7 @@ describe('DebugWithSingleModel', () => {
|
||||
},
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
})
|
||||
@@ -859,7 +873,7 @@ describe('DebugWithSingleModel', () => {
|
||||
completionParams: {},
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
ref.current?.handleRestart()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// Memory and Performance Tests
|
||||
describe('Memory and Performance', () => {
|
||||
it('should properly memoize component', () => {
|
||||
const { rerender } = render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
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(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
// 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(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-with-files'))
|
||||
|
||||
const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
|
||||
await waitFor(() => {
|
||||
expect(ssePost).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
// 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(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-with-files'))
|
||||
|
||||
const { ssePost } = require('@/service/base') as { ssePost: jest.Mock }
|
||||
await waitFor(() => {
|
||||
expect(ssePost).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as React.RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
expect(screen.getByTestId('chat-component')).toBeInTheDocument()
|
||||
const body = ssePost.mock.calls[0][1].body
|
||||
expect(body.files).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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(<UpgradeBtn />)
|
||||
|
||||
// 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(<UpgradeBtn />)
|
||||
|
||||
// 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(<UpgradeBtn isShort />)
|
||||
|
||||
// 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(<UpgradeBtn labelKey="custom.label.key" />)
|
||||
|
||||
// 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(<UpgradeBtn size="s" />)
|
||||
|
||||
// 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(<UpgradeBtn />)
|
||||
|
||||
// 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(<UpgradeBtn size="custom" />)
|
||||
|
||||
// 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(<UpgradeBtn onClick={handleClick} />)
|
||||
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(<UpgradeBtn />)
|
||||
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(<UpgradeBtn loc={loc} />)
|
||||
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(<UpgradeBtn />)
|
||||
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(<UpgradeBtn loc="test-location" />)
|
||||
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(<UpgradeBtn onClick={handleClick} loc={loc} />)
|
||||
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(<UpgradeBtn className={undefined} />)
|
||||
|
||||
// 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(<UpgradeBtn style={undefined} />)
|
||||
|
||||
// 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(<UpgradeBtn onClick={undefined} />)
|
||||
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(<UpgradeBtn loc={undefined} />)
|
||||
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(<UpgradeBtn labelKey={undefined} />)
|
||||
|
||||
// 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(<UpgradeBtn className="" />)
|
||||
|
||||
// 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(<UpgradeBtn loc="" />)
|
||||
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(<UpgradeBtn labelKey="" />)
|
||||
|
||||
// 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(<UpgradeBtn isPlain isShort />)
|
||||
|
||||
// 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(<UpgradeBtn isPlain labelKey="custom.key" />)
|
||||
|
||||
// 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(<UpgradeBtn isShort labelKey="custom.short.key" />)
|
||||
|
||||
// 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(<UpgradeBtn onClick={handleClick} />)
|
||||
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(<UpgradeBtn loc="test" />)
|
||||
const firstRender = screen.getByText(/upgrade to pro/i)
|
||||
|
||||
// Act - Rerender with same props
|
||||
rerender(<UpgradeBtn loc="test" />)
|
||||
|
||||
// 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(<UpgradeBtn labelKey="custom.key" />)
|
||||
expect(screen.getByText(/custom text/i)).toBeInTheDocument()
|
||||
|
||||
// Act - Rerender with different labelKey
|
||||
rerender(<UpgradeBtn labelKey="custom.label.key" />)
|
||||
|
||||
// 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(<UpgradeBtn />)
|
||||
|
||||
// Act - Multiple rapid rerenders
|
||||
for (let i = 0; i < 10; i++)
|
||||
rerender(<UpgradeBtn />)
|
||||
|
||||
// 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 }) => <div>{children}</div>
|
||||
|
||||
const { rerender } = render(
|
||||
<TestWrapper>
|
||||
<UpgradeBtn />
|
||||
</TestWrapper>,
|
||||
)
|
||||
|
||||
const firstElement = screen.getByText(/upgrade to pro/i)
|
||||
|
||||
// Act - Rerender parent with same props
|
||||
rerender(
|
||||
<TestWrapper>
|
||||
<UpgradeBtn />
|
||||
</TestWrapper>,
|
||||
)
|
||||
|
||||
// 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(<UpgradeBtn />)
|
||||
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(<UpgradeBtn onClick={handleClick} loc="integration-test" />)
|
||||
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(() => {
|
||||
|
||||
@@ -172,7 +172,7 @@ describe('InstalledApp', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
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(<InstalledApp id="installed-app-123" />)
|
||||
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(<InstalledApp id="installed-app-123" />)
|
||||
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(<InstalledApp id="installed-app-123" />)
|
||||
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(<InstalledApp id="installed-app-123" />)
|
||||
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(<InstalledApp id="installed-app-123" />)
|
||||
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(<InstalledApp id="installed-app-123" />)
|
||||
// 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(<InstalledApp id="installed-app-123" />)
|
||||
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(<InstalledApp id="installed-app-123" />)
|
||||
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 InstalledApp>).$$typeof
|
||||
expect(componentType).toBeDefined()
|
||||
})
|
||||
|
||||
it('should re-render when props change', () => {
|
||||
const { rerender } = render(<InstalledApp id="installed-app-123" />)
|
||||
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(<InstalledApp id="different-app-456" />)
|
||||
expect(screen.getByText(/different-app-456/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain component stability across re-renders with same props', () => {
|
||||
const { rerender } = render(<InstalledApp id="installed-app-123" />)
|
||||
const initialCallCount = mockUpdateAppInfo.mock.calls.length
|
||||
|
||||
// Rerender with same props - useEffect may still run due to dependencies
|
||||
rerender(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
// 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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
3
web/pnpm-lock.yaml
generated
3
web/pnpm-lock.yaml
generated
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user