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
6 changes: 6 additions & 0 deletions ui/web/components/AuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default function AuthForm({
setLoading(true)
try {
await onTestLogin()
} catch {
// Handled by parent or toast
} finally {
setLoading(false)
}
Expand All @@ -32,6 +34,8 @@ export default function AuthForm({
setLoading(true)
try {
await onSkipAuth()
} catch {
// Handled by parent or toast
} finally {
setLoading(false)
}
Expand All @@ -41,6 +45,8 @@ export default function AuthForm({
setLoading(true)
try {
await onGoogleAuth()
} catch {
// Handled by parent or toast
} finally {
setLoading(false)
}
Expand Down
115 changes: 115 additions & 0 deletions ui/web/tests/unit/adapters/networkStatus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { webNetworkStatus } from '@ui/web/adapters/networkStatus'

describe('webNetworkStatus', () => {
describe('in browser environment', () => {
let addEventListenerSpy: jest.SpyInstance
let removeEventListenerSpy: jest.SpyInstance

beforeEach(() => {
addEventListenerSpy = jest.spyOn(window, 'addEventListener')
removeEventListenerSpy = jest.spyOn(window, 'removeEventListener')
})

afterEach(() => {
jest.restoreAllMocks()
})

describe('isOnline', () => {
it('returns true when navigator.onLine is true', () => {
jest.spyOn(navigator, 'onLine', 'get').mockReturnValue(true)
expect(webNetworkStatus.isOnline()).toBe(true)
})

it('returns false when navigator.onLine is false', () => {
jest.spyOn(navigator, 'onLine', 'get').mockReturnValue(false)
expect(webNetworkStatus.isOnline()).toBe(false)
})
})

describe('subscribe', () => {
it('adds event listeners for online and offline events', () => {
const callback = jest.fn()
const unsubscribe = webNetworkStatus.subscribe(callback)

expect(addEventListenerSpy).toHaveBeenCalledWith('online', expect.any(Function))
expect(addEventListenerSpy).toHaveBeenCalledWith('offline', expect.any(Function))

unsubscribe()
})

it('calls callback(true) when window dispatches online event', () => {
const callback = jest.fn()
const unsubscribe = webNetworkStatus.subscribe(callback)

window.dispatchEvent(new Event('online'))

expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith(true)

unsubscribe()
})

it('calls callback(false) when window dispatches offline event', () => {
const callback = jest.fn()
const unsubscribe = webNetworkStatus.subscribe(callback)

window.dispatchEvent(new Event('offline'))

expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith(false)

unsubscribe()
})

it('removes event listeners when unsubscribe function is called', () => {
const callback = jest.fn()
const unsubscribe = webNetworkStatus.subscribe(callback)

unsubscribe()

expect(removeEventListenerSpy).toHaveBeenCalledWith('online', expect.any(Function))
expect(removeEventListenerSpy).toHaveBeenCalledWith('offline', expect.any(Function))

// Dispatching events after unsubscribe should not invoke callback
window.dispatchEvent(new Event('online'))
window.dispatchEvent(new Event('offline'))

expect(callback).not.toHaveBeenCalled()
})

it('supports multiple independent subscribers', () => {
const callback1 = jest.fn()
const callback2 = jest.fn()

const unsubscribe1 = webNetworkStatus.subscribe(callback1)
const unsubscribe2 = webNetworkStatus.subscribe(callback2)

window.dispatchEvent(new Event('offline'))

expect(callback1).toHaveBeenCalledWith(false)
expect(callback2).toHaveBeenCalledWith(false)

// Unsubscribe only callback1
unsubscribe1()

window.dispatchEvent(new Event('online'))

expect(callback1).toHaveBeenCalledTimes(1)
expect(callback2).toHaveBeenCalledTimes(2)
expect(callback2).toHaveBeenLastCalledWith(true)

unsubscribe2()
})

it('handles multiple unsubscribe calls safely', () => {
const callback = jest.fn()
const unsubscribe = webNetworkStatus.subscribe(callback)

expect(() => {
unsubscribe()
unsubscribe()
}).not.toThrow()
})
})
})
})
132 changes: 132 additions & 0 deletions ui/web/tests/unit/components/AuthForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React from "react"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"

import AuthForm from "@/components/AuthForm"

describe("AuthForm", () => {
const defaultProps = {
onTestLogin: jest.fn(),
onSkipAuth: jest.fn(),
onGoogleAuth: jest.fn(),
}

beforeEach(() => {
jest.clearAllMocks()
})

it("renders Google OAuth button by default and hides test auth options", () => {
render(<AuthForm {...defaultProps} />)

expect(screen.getByRole("button", { name: /continue with google/i })).toBeTruthy()
expect(screen.queryByText(/or test the app/i)).toBeNull()
expect(screen.queryByRole("button", { name: /test login/i })).toBeNull()
expect(screen.queryByRole("button", { name: /skip authentication/i })).toBeNull()
})

it("renders test auth options and divider when enableTestAuth is true", () => {
render(<AuthForm {...defaultProps} enableTestAuth={true} />)

expect(screen.getByRole("button", { name: /continue with google/i })).toBeTruthy()
expect(screen.getByText(/or test the app/i)).toBeTruthy()
expect(screen.getByRole("button", { name: /test login \(persistent\)/i })).toBeTruthy()
expect(screen.getByRole("button", { name: /skip authentication \(quick test\)/i })).toBeTruthy()
})

it("handles Google OAuth button click and manages loading state during pending execution", async () => {
let resolveGoogleAuth!: () => void
const googlePromise = new Promise<void>((resolve) => {
resolveGoogleAuth = resolve
})
const onGoogleAuthMock = jest.fn().mockReturnValue(googlePromise)

render(<AuthForm {...defaultProps} enableTestAuth={true} onGoogleAuth={onGoogleAuthMock} />)

const googleBtn = screen.getByRole("button", { name: /continue with google/i }) as HTMLButtonElement
const testLoginBtn = screen.getByRole("button", { name: /test login/i }) as HTMLButtonElement
const skipAuthBtn = screen.getByRole("button", { name: /skip authentication/i }) as HTMLButtonElement

expect(googleBtn.disabled).toBe(false)
expect(testLoginBtn.disabled).toBe(false)
expect(skipAuthBtn.disabled).toBe(false)

fireEvent.click(googleBtn)

expect(onGoogleAuthMock).toHaveBeenCalledTimes(1)
expect(googleBtn.disabled).toBe(true)
expect(testLoginBtn.disabled).toBe(true)
expect(skipAuthBtn.disabled).toBe(true)

resolveGoogleAuth()

await waitFor(() => {
expect(googleBtn.disabled).toBe(false)
})

expect(testLoginBtn.disabled).toBe(false)
expect(skipAuthBtn.disabled).toBe(false)
})

it("handles Test Login button click and manages loading state", async () => {
let resolveTestLogin!: () => void
const testLoginPromise = new Promise<void>((resolve) => {
resolveTestLogin = resolve
})
const onTestLoginMock = jest.fn().mockReturnValue(testLoginPromise)

render(<AuthForm {...defaultProps} enableTestAuth={true} onTestLogin={onTestLoginMock} />)

const testLoginBtn = screen.getByRole("button", { name: /test login/i }) as HTMLButtonElement

fireEvent.click(testLoginBtn)

expect(onTestLoginMock).toHaveBeenCalledTimes(1)
expect(testLoginBtn.disabled).toBe(true)

resolveTestLogin()

await waitFor(() => {
expect(testLoginBtn.disabled).toBe(false)
})
})

it("handles Skip Authentication button click and manages loading state", async () => {
let resolveSkipAuth!: () => void
const skipAuthPromise = new Promise<void>((resolve) => {
resolveSkipAuth = resolve
})
const onSkipAuthMock = jest.fn().mockReturnValue(skipAuthPromise)

render(<AuthForm {...defaultProps} enableTestAuth={true} onSkipAuth={onSkipAuthMock} />)

const skipAuthBtn = screen.getByRole("button", { name: /skip authentication/i }) as HTMLButtonElement

fireEvent.click(skipAuthBtn)

expect(onSkipAuthMock).toHaveBeenCalledTimes(1)
expect(skipAuthBtn.disabled).toBe(true)

resolveSkipAuth()

await waitFor(() => {
expect(skipAuthBtn.disabled).toBe(false)
})
})

it("resets loading state when authentication action fails or throws error", async () => {
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {})
const onGoogleAuthMock = jest.fn().mockRejectedValue(new Error("Google auth failed"))

render(<AuthForm {...defaultProps} onGoogleAuth={onGoogleAuthMock} />)

const googleBtn = screen.getByRole("button", { name: /continue with google/i }) as HTMLButtonElement

fireEvent.click(googleBtn)

await waitFor(() => {
expect(googleBtn.disabled).toBe(false)
})

expect(onGoogleAuthMock).toHaveBeenCalledTimes(1)
consoleSpy.mockRestore()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
Loading
Loading