Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .axioma/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
**Learning:** To achieve 100% branch coverage in hooks or functions that use default parameters (e.g., `useThrottle(value, limit = 500)`), unit tests must explicitly invoke the function without the optional arguments. Simply relying on tests that provide values for all arguments leaves the default assignment branch uncovered.
**Action:** Always include a test case that omits optional arguments to ensure default parameter logic is verified and coverage is maximized.

## 2024-06-20 - [Consistent Code Formatting and Micro-Improvements]

**Learning:** The project's `.prettierrc.json` (4 spaces, no semicolons) may conflict with the existing style of some older files (2 spaces, semicolons). Running global formatters on these files can create large diffs that exceed "micro-improvement" constraints (e.g., 50-line limit).
**Action:** When performing micro-improvements, prefer manual formatting that matches the file's current style if a full reformat would exceed the line-count limit, or ensure reformatting is justified by the project's official config.

## 2024-06-25 - [Robust Timer Input Validation]

**Learning:** Browser timer APIs like `setInterval` and `setTimeout` have inconsistent behaviors when receiving `NaN` or unexpected objects (like `Date` for intervals), often defaulting to 1ms or 0ms without warning. Explicitly validating these inputs and providing fallback values (e.g., 1000ms for intervals) with `console.warn` ensures predictable behavior and improves developer experience.
Expand Down
256 changes: 256 additions & 0 deletions lib/hooks/useAISummarize.coverage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { useAISummarize } from './useAISummarize'

describe('useAISummarize Coverage', () => {
const mockSummarizer = {
summarize: vi.fn(),
summarizeStreaming: vi.fn(),
destroy: vi.fn(),
}

const mockSummarizerCreate = vi.fn()
const mockAvailability = vi.fn()

beforeEach(() => {
vi.stubGlobal('navigator', {
userActivation: { isActive: true },
})

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const SummarizerConstructor = function () {} as any
SummarizerConstructor.availability = mockAvailability
SummarizerConstructor.create = mockSummarizerCreate

vi.stubGlobal('Summarizer', SummarizerConstructor)
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).Summarizer = SummarizerConstructor
}
mockAvailability.mockResolvedValue('readily')
mockSummarizerCreate.mockResolvedValue(mockSummarizer)
})

afterEach(() => {
vi.unstubAllGlobals()
vi.clearAllMocks()
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (window as any).Summarizer
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (window as any).LanguageDetector
}
})

it('should handle base constructors in createSummarizer', async () => {
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).Summarizer = Object
}
const { result } = renderHook(() => useAISummarize())

await act(async () => {
await result.current.summarize('text')
})

expect(result.current.status).toBe('error')
expect(result.current.error?.message).toContain(
'Summarizer is not available',
)
})

it('should handle unavailable status in createSummarizer', async () => {
mockAvailability.mockResolvedValue('unavailable')
const { result } = renderHook(() => useAISummarize())

await act(async () => {
await result.current.summarize('text')
})

expect(result.current.status).toBe('error')
expect(result.current.error?.message).toContain(
'Summarizer is not available',
)
})

it('should handle base constructors in detectLanguageFromText', async () => {
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).LanguageDetector = Object
}
const { result } = renderHook(() =>
useAISummarize({ outputLanguage: 'auto' }),
)

await act(async () => {
await result.current.summarize('text')
})

// Should fallback to 'en'
expect(mockSummarizerCreate).toHaveBeenCalledWith(
expect.objectContaining({ outputLanguage: 'en' }),
)
})

it('should handle unavailable LanguageDetector in detectLanguageFromText', async () => {
const mockLanguageAvailability = vi
.fn()
.mockResolvedValue('unavailable')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const LanguageDetectorConstructor = function () {} as any
LanguageDetectorConstructor.availability = mockLanguageAvailability

vi.stubGlobal('LanguageDetector', LanguageDetectorConstructor)
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).LanguageDetector = LanguageDetectorConstructor
}

const { result } = renderHook(() =>
useAISummarize({ outputLanguage: 'auto' }),
)

await act(async () => {
await result.current.summarize('text')
})

expect(mockSummarizerCreate).toHaveBeenCalledWith(
expect.objectContaining({ outputLanguage: 'en' }),
)
})

it('should handle missing userActivation in detectLanguageFromText', async () => {
vi.stubGlobal('navigator', {
userActivation: { isActive: false },
})

const mockLanguageDetectorCreate = vi.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const LanguageDetectorConstructor = function () {} as any
LanguageDetectorConstructor.availability = vi
.fn()
.mockResolvedValue('readily')
LanguageDetectorConstructor.create = mockLanguageDetectorCreate

vi.stubGlobal('LanguageDetector', LanguageDetectorConstructor)

const { result } = renderHook(() =>
useAISummarize({ outputLanguage: 'auto' }),
)

await act(async () => {
await result.current.summarize('text')
})

// Should return 'en' and NOT call LanguageDetector.create
expect(mockLanguageDetectorCreate).not.toHaveBeenCalled()
expect(result.current.status).toBe('error')
expect(result.current.error?.message).toContain(
'User activation required',
)
})

it('should handle explicit outputLanguage', async () => {
const { result } = renderHook(() =>
useAISummarize({ outputLanguage: 'ja' }),
)

await act(async () => {
await result.current.summarize('text')
})

expect(mockSummarizerCreate).toHaveBeenCalledWith(
expect.objectContaining({ outputLanguage: 'ja' }),
)
})

it('should handle context in summarize', async () => {
const { result } = renderHook(() => useAISummarize())

await act(async () => {
await result.current.summarize('text', 'custom context')
})

expect(mockSummarizer.summarize).toHaveBeenCalledWith(
'text',
expect.objectContaining({ context: 'custom context' }),
)
})

it('should handle warmup error', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {})
mockSummarizerCreate.mockRejectedValue(new Error('Warmup failed'))

renderHook(() => useAISummarize({ warmup: true }))

await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to warmup summarizer:',
expect.any(Error),
)
})
consoleSpy.mockRestore()
})

it('should handle missing create method in detectLanguageFromText', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const LanguageDetectorConstructor = function () {} as any
LanguageDetectorConstructor.availability = vi
.fn()
.mockResolvedValue('readily')
// create is missing

vi.stubGlobal('LanguageDetector', LanguageDetectorConstructor)
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).LanguageDetector = LanguageDetectorConstructor
}

const { result } = renderHook(() =>
useAISummarize({ outputLanguage: 'auto' }),
)

await act(async () => {
await result.current.summarize('text')
})

expect(mockSummarizerCreate).toHaveBeenCalledWith(
expect.objectContaining({ outputLanguage: 'en' }),
)
})

it('should handle empty results in detectLanguageFromText', async () => {
const mockLanguageDetector = {
detect: vi.fn().mockResolvedValue([]),
destroy: vi.fn(),
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const LanguageDetectorConstructor = function () {} as any
LanguageDetectorConstructor.availability = vi
.fn()
.mockResolvedValue('readily')
LanguageDetectorConstructor.create = vi
.fn()
.mockResolvedValue(mockLanguageDetector)

vi.stubGlobal('LanguageDetector', LanguageDetectorConstructor)
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).LanguageDetector = LanguageDetectorConstructor
}

const { result } = renderHook(() =>
useAISummarize({ outputLanguage: 'auto' }),
)

await act(async () => {
await result.current.summarize('text')
})

expect(mockSummarizerCreate).toHaveBeenCalledWith(
expect.objectContaining({ outputLanguage: 'en' }),
)
})
})
Loading
Loading