diff --git a/web/app/components/app/log/index.spec.tsx b/web/app/components/app/log/index.spec.tsx
new file mode 100644
index 0000000000..6175115c70
--- /dev/null
+++ b/web/app/components/app/log/index.spec.tsx
@@ -0,0 +1,200 @@
+import type { ReactNode } from 'react'
+import type { ChatConversationGeneralDetail, ChatConversationsResponse } from '@/models/log'
+import type { App, AppIconType } from '@/types/app'
+import { render, screen } from '@testing-library/react'
+import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
+import { APP_PAGE_LIMIT } from '@/config'
+import { AppModeEnum } from '@/types/app'
+import Logs from './index'
+
+const mockUseChatConversations = vi.fn()
+const mockUseCompletionConversations = vi.fn()
+const mockUseAnnotationsCount = vi.fn()
+
+const mockRouterPush = vi.fn()
+const mockRouterReplace = vi.fn()
+
+const mockAppStoreState = {
+ setShowPromptLogModal: vi.fn(),
+ setShowAgentLogModal: vi.fn(),
+ setShowMessageLogModal: vi.fn(),
+}
+
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({
+ push: mockRouterPush,
+ replace: mockRouterReplace,
+ }),
+ usePathname: () => '/apps/app-123/logs',
+ useSearchParams: () => new URLSearchParams(),
+}))
+
+vi.mock('@/service/use-log', () => ({
+ useChatConversations: (args: unknown) => mockUseChatConversations(args),
+ useCompletionConversations: (args: unknown) => mockUseCompletionConversations(args),
+ useAnnotationsCount: () => mockUseAnnotationsCount(),
+ useChatConversationDetail: () => ({ data: undefined }),
+ useCompletionConversationDetail: () => ({ data: undefined }),
+}))
+
+vi.mock('@/service/log', () => ({
+ fetchChatMessages: vi.fn(),
+ updateLogMessageAnnotations: vi.fn(),
+ updateLogMessageFeedbacks: vi.fn(),
+}))
+
+vi.mock('@/context/app-context', () => ({
+ useAppContext: () => ({
+ userProfile: { timezone: 'UTC' },
+ }),
+}))
+
+vi.mock('@/app/components/app/store', () => ({
+ useStore: (selector: (state: typeof mockAppStoreState) => unknown) => selector(mockAppStoreState),
+}))
+
+const renderWithAdapter = (ui: ReactNode, searchParams = '') => {
+ return render(
+
+ {ui}
+ ,
+ )
+}
+
+const createMockApp = (overrides: Partial = {}): App => ({
+ id: 'app-123',
+ name: 'Test App',
+ description: 'Test app description',
+ author_name: 'Test Author',
+ icon_type: 'emoji' as AppIconType,
+ icon: ':icon:',
+ icon_background: '#FFEAD5',
+ icon_url: null,
+ use_icon_as_answer_icon: false,
+ mode: AppModeEnum.CHAT,
+ enable_site: true,
+ enable_api: true,
+ api_rpm: 60,
+ api_rph: 3600,
+ is_demo: false,
+ model_config: {} as App['model_config'],
+ app_model_config: {} as App['app_model_config'],
+ created_at: Date.now(),
+ updated_at: Date.now(),
+ site: {
+ access_token: 'token',
+ app_base_url: 'https://example.com',
+ } as App['site'],
+ api_base_url: 'https://api.example.com',
+ tags: [],
+ access_mode: 'public_access' as App['access_mode'],
+ ...overrides,
+})
+
+const createChatConversation = (overrides: Partial = {}): ChatConversationGeneralDetail => ({
+ id: 'conversation-1',
+ status: 'normal',
+ from_source: 'api',
+ from_end_user_id: 'user-1',
+ from_end_user_session_id: 'session-1',
+ from_account_id: 'account-1',
+ read_at: new Date(),
+ created_at: 1700000000,
+ updated_at: 1700000001,
+ user_feedback_stats: { like: 0, dislike: 0 },
+ admin_feedback_stats: { like: 0, dislike: 0 },
+ model_config: {
+ provider: 'openai',
+ model_id: 'gpt-4',
+ configs: { prompt_template: '' },
+ },
+ summary: 'Conversation summary',
+ message_count: 1,
+ annotated: false,
+ ...overrides,
+})
+
+const createChatConversationsResponse = (overrides: Partial = {}): ChatConversationsResponse => ({
+ data: [createChatConversation()],
+ has_more: false,
+ limit: APP_PAGE_LIMIT,
+ total: 1,
+ page: 1,
+ ...overrides,
+})
+
+// Logs page: loading, empty, and data states.
+describe('Logs', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ globalThis.innerWidth = 1024
+
+ mockUseAnnotationsCount.mockReturnValue({
+ data: { count: 0 },
+ isLoading: false,
+ })
+
+ mockUseChatConversations.mockReturnValue({
+ data: undefined,
+ refetch: vi.fn(),
+ })
+
+ mockUseCompletionConversations.mockReturnValue({
+ data: undefined,
+ refetch: vi.fn(),
+ })
+ })
+
+ // Loading behavior when no data yet.
+ describe('Rendering', () => {
+ it('should render loading state when conversations are undefined', () => {
+ // Arrange
+ const appDetail = createMockApp()
+
+ // Act
+ renderWithAdapter()
+
+ // Assert
+ expect(screen.getByRole('status')).toBeInTheDocument()
+ })
+
+ it('should render empty state when there are no conversations', () => {
+ // Arrange
+ mockUseChatConversations.mockReturnValue({
+ data: createChatConversationsResponse({ data: [], total: 0 }),
+ refetch: vi.fn(),
+ })
+ const appDetail = createMockApp()
+
+ // Act
+ renderWithAdapter()
+
+ // Assert
+ expect(screen.getByText('appLog.table.empty.element.title')).toBeInTheDocument()
+ expect(screen.queryByRole('status')).not.toBeInTheDocument()
+ })
+ })
+
+ // Data rendering behavior.
+ describe('Props', () => {
+ it('should render list with pagination when conversations exist', () => {
+ // Arrange
+ mockUseChatConversations.mockReturnValue({
+ data: createChatConversationsResponse({ total: APP_PAGE_LIMIT + 1 }),
+ refetch: vi.fn(),
+ })
+ const appDetail = createMockApp()
+
+ // Act
+ renderWithAdapter(, '?page=0&limit=0')
+
+ // Assert
+ expect(screen.getByText('appLog.table.header.summary')).toBeInTheDocument()
+ expect(screen.getByText('25')).toBeInTheDocument()
+
+ const firstCallArgs = mockUseChatConversations.mock.calls[0]?.[0]
+ expect(firstCallArgs.params.page).toBe(1)
+ expect(firstCallArgs.params.limit).toBe(APP_PAGE_LIMIT)
+ })
+ })
+})
diff --git a/web/app/components/datasets/documents/hooks/use-document-list-query-state.spec.tsx b/web/app/components/datasets/documents/hooks/use-document-list-query-state.spec.tsx
new file mode 100644
index 0000000000..514a859d59
--- /dev/null
+++ b/web/app/components/datasets/documents/hooks/use-document-list-query-state.spec.tsx
@@ -0,0 +1,133 @@
+import type { ReactNode } from 'react'
+import { act, renderHook, waitFor } from '@testing-library/react'
+import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
+import useDocumentListQueryState from './use-document-list-query-state'
+
+const renderWithAdapter = (searchParams = '') => {
+ const wrapper = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ )
+
+ return renderHook(() => useDocumentListQueryState(), { wrapper })
+}
+
+// Document list query state: defaults, sanitization, and update actions.
+describe('useDocumentListQueryState', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ // Default query values.
+ describe('Rendering', () => {
+ it('should return default query values when URL params are missing', () => {
+ // Arrange
+ const { result } = renderWithAdapter()
+
+ // Act
+ const { query } = result.current
+
+ // Assert
+ expect(query).toEqual({
+ page: 1,
+ limit: 10,
+ keyword: '',
+ status: 'all',
+ sort: '-created_at',
+ })
+ })
+ })
+
+ // URL sanitization behavior.
+ describe('Edge Cases', () => {
+ it('should sanitize invalid URL query values', () => {
+ // Arrange
+ const { result } = renderWithAdapter('?page=0&limit=500&keyword=%20%20&status=invalid&sort=bad')
+
+ // Act
+ const { query } = result.current
+
+ // Assert
+ expect(query).toEqual({
+ page: 1,
+ limit: 10,
+ keyword: '',
+ status: 'all',
+ sort: '-created_at',
+ })
+ })
+ })
+
+ // Query update actions.
+ describe('User Interactions', () => {
+ it('should normalize query updates', async () => {
+ // Arrange
+ const { result } = renderWithAdapter()
+
+ // Act
+ act(() => {
+ result.current.updateQuery({
+ page: 0,
+ limit: 200,
+ keyword: ' search ',
+ status: 'invalid',
+ sort: 'hit_count',
+ })
+ })
+
+ // Assert
+ await waitFor(() => {
+ expect(result.current.query).toEqual({
+ page: 1,
+ limit: 10,
+ keyword: ' search ',
+ status: 'all',
+ sort: 'hit_count',
+ })
+ })
+ })
+
+ it('should reset query values to defaults', async () => {
+ // Arrange
+ const { result } = renderWithAdapter('?page=2&limit=25&keyword=hello&status=enabled&sort=hit_count')
+
+ // Act
+ act(() => {
+ result.current.resetQuery()
+ })
+
+ // Assert
+ await waitFor(() => {
+ expect(result.current.query).toEqual({
+ page: 1,
+ limit: 10,
+ keyword: '',
+ status: 'all',
+ sort: '-created_at',
+ })
+ })
+ })
+ })
+
+ // Callback stability.
+ describe('Performance', () => {
+ it('should keep action callbacks stable across updates', async () => {
+ // Arrange
+ const { result } = renderWithAdapter()
+ const initialUpdate = result.current.updateQuery
+ const initialReset = result.current.resetQuery
+
+ // Act
+ act(() => {
+ result.current.updateQuery({ page: 2 })
+ })
+
+ // Assert
+ await waitFor(() => {
+ expect(result.current.updateQuery).toBe(initialUpdate)
+ expect(result.current.resetQuery).toBe(initialReset)
+ })
+ })
+ })
+})