Skip to content
Merged
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
83 changes: 11 additions & 72 deletions apps/mobile/__tests__/components/chat/action-chips.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,24 @@
}

describe('ActionChips (mobile)', () => {
it('renders successful Create chip as a Pressable when onChipClick is provided', () => {
it.each<{ name: string; overrides: Partial<ActionResult>; handler: boolean; expected: number }>([
{ name: 'renders successful Create chip as a Pressable when onChipClick is provided', overrides: { type: 'CreateHabit', status: 'Success', entityId: 'h-1' }, handler: true, expected: 1 },
{ name: 'does not render Delete chip as Pressable even with handler', overrides: { type: 'DeleteHabit', status: 'Success', entityId: 'h-1' }, handler: true, expected: 0 },
{ name: 'does not render DeleteGoal chip as Pressable even with handler', overrides: { type: 'DeleteGoal', status: 'Success', entityId: 'g-1' }, handler: true, expected: 0 },
{ name: 'does not render Failed chip as Pressable even with handler', overrides: { type: 'CreateHabit', status: 'Failed', entityId: 'h-1', error: 'oops' }, handler: true, expected: 0 },
{ name: 'does not render as Pressable when no handler is provided', overrides: { type: 'CreateHabit', status: 'Success', entityId: 'h-1' }, handler: false, expected: 0 },
{ name: 'does not render chip with null entityId as Pressable', overrides: { type: 'CreateHabit', status: 'Success', entityId: null }, handler: true, expected: 0 },
])('$name', ({ overrides, handler, expected }) => {
let tree: any
TestRenderer.act(() => {
tree = TestRenderer.create(
<ActionChips
actions={[makeAction({ type: 'CreateHabit', status: 'Success', entityId: 'h-1' })]}
onChipClick={() => {}}
actions={[makeAction(overrides)]}
onChipClick={handler ? () => {} : undefined}
/>,
)
})
const pressables = findPressableByType(tree.root)
expect(pressables.length).toBe(1)
expect(findPressableByType(tree.root).length).toBe(expected)

Check warning on line 104 in apps/mobile/__tests__/components/chat/action-chips.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(findPressableByType(tree.root)).toHaveLength(expected)" over this generic assertion for better reporting; it works on any object with a numeric length property.

See more on https://sonarcloud.io/project/issues?id=thomasluizon_orbit-ui-mobile&issues=AZ9bRXUAwlJpyAD2kbcD&open=AZ9bRXUAwlJpyAD2kbcD&pullRequest=482
})

it('calls onChipClick with entityId and actionType on press', () => {
Expand Down Expand Up @@ -134,20 +140,6 @@
expect(onChipClick).toHaveBeenCalledWith('g-42', 'UpdateGoal')
})

it('does not render Delete chip as Pressable even with handler', () => {
let tree: any
TestRenderer.act(() => {
tree = TestRenderer.create(
<ActionChips
actions={[makeAction({ type: 'DeleteHabit', status: 'Success', entityId: 'h-1' })]}
onChipClick={() => {}}
/>,
)
})
const pressables = findPressableByType(tree.root)
expect(pressables.length).toBe(0)
})

it('does not render tag mutation chips as Pressable even with handler', () => {
for (const type of ['CreateTag', 'UpdateTag', 'DeleteTag']) {
let tree: any
Expand All @@ -163,47 +155,6 @@
}
})

it('does not render DeleteGoal chip as Pressable even with handler', () => {
let tree: any
TestRenderer.act(() => {
tree = TestRenderer.create(
<ActionChips
actions={[makeAction({ type: 'DeleteGoal', status: 'Success', entityId: 'g-1' })]}
onChipClick={() => {}}
/>,
)
})
const pressables = findPressableByType(tree.root)
expect(pressables.length).toBe(0)
})

it('does not render Failed chip as Pressable even with handler', () => {
let tree: any
TestRenderer.act(() => {
tree = TestRenderer.create(
<ActionChips
actions={[
makeAction({ type: 'CreateHabit', status: 'Failed', entityId: 'h-1', error: 'oops' }),
]}
onChipClick={() => {}}
/>,
)
})
expect(findPressableByType(tree.root).length).toBe(0)
})

it('does not render as Pressable when no handler is provided', () => {
let tree: any
TestRenderer.act(() => {
tree = TestRenderer.create(
<ActionChips
actions={[makeAction({ type: 'CreateHabit', status: 'Success', entityId: 'h-1' })]}
/>,
)
})
expect(findPressableByType(tree.root).length).toBe(0)
})

it('renders localized labels for the new tag and reorder action types', () => {
const cases: Array<[string, string]> = [
['CreateTag', 'chat.action.createdTag'],
Expand All @@ -228,16 +179,4 @@
}
})

it('does not render chip with null entityId as Pressable', () => {
let tree: any
TestRenderer.act(() => {
tree = TestRenderer.create(
<ActionChips
actions={[makeAction({ type: 'CreateHabit', status: 'Success', entityId: null })]}
onChipClick={() => {}}
/>,
)
})
expect(findPressableByType(tree.root).length).toBe(0)
})
})
66 changes: 8 additions & 58 deletions apps/mobile/__tests__/components/chat/clarification-card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,13 @@ describe('ClarificationCard (mobile)', () => {
expect(successNodes.length).toBeGreaterThan(0)
})

it('shows expired-error text when resolve throws a 404', async () => {
mutateAsync.mockRejectedValueOnce(Object.assign(new Error('expired'), { status: 404 }))
it.each<{ name: string; error: Error; expectedKey: string }>([
{ name: 'shows expired-error text when resolve throws a 404', error: Object.assign(new Error('expired'), { status: 404 }), expectedKey: 'habits.clarification.errorExpired' },
{ name: 'shows generic-error text when resolve throws a non-404 error', error: new Error('network error'), expectedKey: 'habits.clarification.errorGeneric' },
{ name: 'shows already-resolved error when resolve throws a 409', error: Object.assign(new Error('conflict'), { status: 409 }), expectedKey: 'habits.clarification.errorAlreadyResolved' },
{ name: 'shows expired-error text when resolve throws a 410 Gone', error: Object.assign(new Error('gone'), { status: 410 }), expectedKey: 'habits.clarification.errorExpired' },
])('$name', async ({ error, expectedKey }) => {
mutateAsync.mockRejectedValueOnce(error)

let tree!: TestInstance
await TestRenderer.act(async () => {
Expand All @@ -200,62 +205,7 @@ describe('ClarificationCard (mobile)', () => {
await firstButton.props.onPress!()
})

const errorNodes = findTextNodesWithChild(tree.root, 'habits.clarification.errorExpired')
expect(errorNodes.length).toBeGreaterThan(0)
})

it('shows generic-error text when resolve throws a non-404 error', async () => {
// No .status property — exercises the `status === 0` fallback path.
mutateAsync.mockRejectedValueOnce(new Error('network error'))

let tree!: TestInstance
await TestRenderer.act(async () => {
tree = TestRenderer.create(<ClarificationCard clarificationRequest={baseClarification} />)
})

const [firstButton] = findPressables(tree.root)
if (!firstButton?.props.onPress) throw new Error('first button missing onPress')
await TestRenderer.act(async () => {
await firstButton.props.onPress!()
})

const errorNodes = findTextNodesWithChild(tree.root, 'habits.clarification.errorGeneric')
expect(errorNodes.length).toBeGreaterThan(0)
})

it('shows already-resolved error when resolve throws a 409', async () => {
mutateAsync.mockRejectedValueOnce(Object.assign(new Error('conflict'), { status: 409 }))

let tree!: TestInstance
await TestRenderer.act(async () => {
tree = TestRenderer.create(<ClarificationCard clarificationRequest={baseClarification} />)
})

const [firstButton] = findPressables(tree.root)
if (!firstButton?.props.onPress) throw new Error('first button missing onPress')
await TestRenderer.act(async () => {
await firstButton.props.onPress!()
})

const errorNodes = findTextNodesWithChild(tree.root, 'habits.clarification.errorAlreadyResolved')
expect(errorNodes.length).toBeGreaterThan(0)
})

it('shows expired-error text when resolve throws a 410 Gone', async () => {
mutateAsync.mockRejectedValueOnce(Object.assign(new Error('gone'), { status: 410 }))

let tree!: TestInstance
await TestRenderer.act(async () => {
tree = TestRenderer.create(<ClarificationCard clarificationRequest={baseClarification} />)
})

const [firstButton] = findPressables(tree.root)
if (!firstButton?.props.onPress) throw new Error('first button missing onPress')
await TestRenderer.act(async () => {
await firstButton.props.onPress!()
})

const errorNodes = findTextNodesWithChild(tree.root, 'habits.clarification.errorExpired')
const errorNodes = findTextNodesWithChild(tree.root, expectedKey)
expect(errorNodes.length).toBeGreaterThan(0)
})

Expand Down
34 changes: 8 additions & 26 deletions apps/web/__tests__/app/today-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,37 +366,19 @@ describe('TodayPage bulk parent prompts', () => {
})
})

it('navigates to the previous day with a date query param', () => {
dateParamState.value = '2026-04-07'
uiState.isSelectMode = false

renderPage()

fireEvent.click(screen.getByLabelText('dates.previousDay'))

expect(mockRouterPush).toHaveBeenCalledWith('/?date=2026-04-06')
})

it('navigates to the next day with a date query param', () => {
dateParamState.value = '2026-04-07'
uiState.isSelectMode = false

renderPage()

fireEvent.click(screen.getByLabelText('dates.nextDay'))

expect(mockRouterPush).toHaveBeenCalledWith('/?date=2026-04-08')
})

it('returns to today via the bare route when pressing the date label', () => {
dateParamState.value = '2026-04-06'
it.each([
{ name: 'navigates to the previous day with a date query param', date: '2026-04-07', label: 'dates.previousDay', target: '/?date=2026-04-06' },
{ name: 'navigates to the next day with a date query param', date: '2026-04-07', label: 'dates.nextDay', target: '/?date=2026-04-08' },
{ name: 'returns to today via the bare route when pressing the date label', date: '2026-04-06', label: 'dates.goToToday', target: '/' },
])('$name', ({ date, label, target }) => {
dateParamState.value = date
uiState.isSelectMode = false

renderPage()

fireEvent.click(screen.getByLabelText('dates.goToToday'))
fireEvent.click(screen.getByLabelText(label))

expect(mockRouterPush).toHaveBeenCalledWith('/')
expect(mockRouterPush).toHaveBeenCalledWith(target)
})

it('renders today on the bare route and the pinned day on a date deep link', () => {
Expand Down
33 changes: 10 additions & 23 deletions apps/web/__tests__/components/chat/conflict-warning.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,16 @@ function makeWarning(overrides: Partial<ConflictWarningType> = {}): ConflictWarn
}

describe('ConflictWarning', () => {
it('renders with HIGH severity styling', () => {
const { container } = render(
<ConflictWarning warning={makeWarning({ severity: 'HIGH' })} />,
)
const wrapper = container.firstElementChild
expect(wrapper?.getAttribute('data-severity')).toBe('HIGH')
})

it('renders with MEDIUM severity styling', () => {
const { container } = render(
<ConflictWarning warning={makeWarning({ severity: 'MEDIUM' })} />,
)
const wrapper = container.firstElementChild
expect(wrapper?.getAttribute('data-severity')).toBe('MEDIUM')
})

it('renders with LOW severity styling', () => {
const { container } = render(
<ConflictWarning warning={makeWarning({ severity: 'LOW' })} />,
)
const wrapper = container.firstElementChild
expect(wrapper?.getAttribute('data-severity')).toBe('LOW')
})
it.each(['HIGH', 'MEDIUM', 'LOW'] as const)(
'renders with %s severity styling',
(severity) => {
const { container } = render(
<ConflictWarning warning={makeWarning({ severity })} />,
)
const wrapper = container.firstElementChild
expect(wrapper?.getAttribute('data-severity')).toBe(severity)
},
)

it('renders conflicting habits', () => {
const warning = makeWarning({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,15 @@ describe('LevelUpOverlay', () => {
expect(alert).toBeInTheDocument()
})

it('displays the new level number padded to two digits', () => {
it.each([
{ name: 'displays the new level number padded to two digits', text: '05' },
{ name: 'displays level up title', text: 'gamification.levelUp.title' },
{ name: 'displays steady hand quiet copy', text: 'gamification.levelUp.steadyHand' },
])('$name', ({ text }) => {
render(
<LevelUpOverlay leveledUp={true} newLevel={5} onClear={vi.fn()} />,
)
expect(document.body.textContent).toContain('05')
})

it('displays level up title', () => {
render(
<LevelUpOverlay leveledUp={true} newLevel={5} onClear={vi.fn()} />,
)
expect(document.body.textContent).toContain('gamification.levelUp.title')
})

it('displays steady hand quiet copy', () => {
render(
<LevelUpOverlay leveledUp={true} newLevel={5} onClear={vi.fn()} />,
)
expect(document.body.textContent).toContain('gamification.levelUp.steadyHand')
expect(document.body.textContent).toContain(text)
})

it('renders the rotating orbit ring SVG', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,14 @@ describe('StreakCelebration', () => {
expect(document.querySelector('[role="status"]')).toBeInTheDocument()
})

it('displays the streak count', () => {
mockStreakCelebration = { streak: 14 }
it.each([
{ name: 'displays the streak count', streak: 14, text: '14' },
{ name: 'shows milestone encouragement for milestone streaks', streak: 30, text: 'streakDisplay.celebration.milestone' },
{ name: 'renders the Streak eyebrow label', streak: 3, text: 'streakDisplay.celebration.eyebrow' },
])('$name', ({ streak, text }) => {
mockStreakCelebration = { streak }
render(<StreakCelebration />)
expect(document.body.textContent).toContain('14')
})

it('shows milestone encouragement for milestone streaks', () => {
mockStreakCelebration = { streak: 30 }
render(<StreakCelebration />)
expect(document.body.textContent).toContain('streakDisplay.celebration.milestone')
expect(document.body.textContent).toContain(text)
})

it('renders Saturn-ring concentric rings via RingMotif', () => {
Expand All @@ -68,12 +66,6 @@ describe('StreakCelebration', () => {
expect(rings?.querySelectorAll('span').length).toBe(4)
})

it('renders the Streak eyebrow label', () => {
mockStreakCelebration = { streak: 3 }
render(<StreakCelebration />)
expect(document.body.textContent).toContain('streakDisplay.celebration.eyebrow')
})

it('dismisses on click', () => {
mockStreakCelebration = { streak: 5 }
render(<StreakCelebration />)
Expand Down
Loading
Loading