-
Notifications
You must be signed in to change notification settings - Fork 41
test(hooks): add unit tests for @asgardeo/react hooks to cover edge c… #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| /** | ||
| * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). | ||
| * | ||
| * WSO2 LLC. licenses this file to you under the Apache License, | ||
| * Version 2.0 (the "License"); you may not use this file except | ||
| * in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| import { renderHook } from '@testing-library/react'; | ||
| import useBranding from '../hooks/useBranding'; | ||
| import { vi, describe, it, expect, afterEach } from 'vitest'; | ||
|
|
||
| // Mock the context and make default export a vi.fn() | ||
| vi.mock('../contexts/Branding/useBrandingContext', () => { | ||
| return { default: vi.fn() }; | ||
| }); | ||
|
|
||
| // Import the mocked function | ||
| import useBrandingContext from '../contexts/Branding/useBrandingContext'; | ||
|
|
||
| describe('useBranding hook', () => { | ||
| const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { }); | ||
|
|
||
| afterEach(() => { | ||
| vi.resetAllMocks(); | ||
| }); | ||
|
|
||
| it('returns values from context when available', () => { | ||
| const mockReturn = { | ||
| brandingPreference: { name: 'TestBrand' }, | ||
| theme: { colors: { primary: { main: '#000' } } }, | ||
| activeTheme: 'light', | ||
| isLoading: false, | ||
| error: null, | ||
| fetchBranding: vi.fn(), | ||
| refetch: vi.fn(), | ||
| }; | ||
|
|
||
| (useBrandingContext as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockReturn); | ||
|
|
||
| const { result } = renderHook(() => useBranding()); | ||
|
|
||
| expect(result.current.brandingPreference).toEqual(mockReturn.brandingPreference); | ||
| expect(result.current.theme).toEqual(mockReturn.theme); | ||
| expect(result.current.activeTheme).toBe('light'); | ||
| expect(result.current.isLoading).toBe(false); | ||
| expect(result.current.error).toBeNull(); | ||
| expect(typeof result.current.fetchBranding).toBe('function'); | ||
| expect(typeof result.current.refetch).toBe('function'); | ||
| }); | ||
|
|
||
| it('returns default values when context is missing', async () => { | ||
| (useBrandingContext as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => { | ||
| throw new Error('Provider missing'); | ||
| }); | ||
|
|
||
| const { result } = renderHook(() => useBranding()); | ||
|
|
||
| expect(result.current.brandingPreference).toBeNull(); | ||
| expect(result.current.theme).toBeNull(); | ||
| expect(result.current.activeTheme).toBeNull(); | ||
| expect(result.current.isLoading).toBe(false); | ||
| expect(result.current.error).toBeInstanceOf(Error); | ||
|
|
||
| await expect(result.current.fetchBranding()).resolves.toBeUndefined(); | ||
| await expect(result.current.refetch()).resolves.toBeUndefined(); | ||
|
|
||
| expect(consoleWarnSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining('useBranding: BrandingProvider not available') | ||
| ); | ||
| }); | ||
|
|
||
| it('accepts a config object without breaking', () => { | ||
| (useBrandingContext as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => { | ||
| throw new Error('Provider missing'); | ||
| }); | ||
|
|
||
| const { result } = renderHook(() => useBranding({ locale: 'en', autoFetch: true })); | ||
|
|
||
| expect(result.current.brandingPreference).toBeNull(); | ||
| expect(result.current.theme).toBeNull(); | ||
| expect(result.current.activeTheme).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /** | ||
| * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). | ||
| * | ||
| * WSO2 LLC. licenses this file to you under the Apache License, | ||
| * Version 2.0 (the "License"); you may not use this file except | ||
| * in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| import useBrowserUrl from "../hooks/useBrowserUrl"; | ||
| import { hasAuthParamsInUrl } from "@asgardeo/browser"; | ||
| import { vi, describe, it, expect, beforeEach } from "vitest"; | ||
|
|
||
| // Mock the module | ||
| vi.mock("@asgardeo/browser", () => ({ | ||
| hasAuthParamsInUrl: vi.fn(), | ||
| })); | ||
|
|
||
| describe("useBrowserUrl hook", () => { | ||
| const { hasAuthParams } = useBrowserUrl(); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it("returns true if hasAuthParamsInUrl returns true and URL matches afterSignInUrl", () => { | ||
| (hasAuthParamsInUrl as unknown as ReturnType<typeof vi.fn>).mockReturnValue(true); | ||
| const url = new URL("https://example.com/callback"); | ||
| const afterSignInUrl = "https://example.com/callback"; | ||
| expect(hasAuthParams(url, afterSignInUrl)).toBe(true); | ||
| }); | ||
|
|
||
| it("returns false if hasAuthParamsInUrl returns false and no error param exists", () => { | ||
| (hasAuthParamsInUrl as unknown as ReturnType<typeof vi.fn>).mockReturnValue(false); | ||
| const url = new URL("https://example.com/callback"); | ||
| const afterSignInUrl = "https://example.com/other"; | ||
| expect(hasAuthParams(url, afterSignInUrl)).toBe(false); | ||
| }); | ||
|
|
||
| it("returns true if URL contains error param", () => { | ||
| (hasAuthParamsInUrl as unknown as ReturnType<typeof vi.fn>).mockReturnValue(false); | ||
| const url = new URL("https://example.com/callback?error=access_denied"); | ||
| const afterSignInUrl = "https://example.com/other"; | ||
| expect(hasAuthParams(url, afterSignInUrl)).toBe(true); | ||
| }); | ||
|
|
||
| it("returns false if URL does not match afterSignInUrl and no error param and hasAuthParamsInUrl false", () => { | ||
| (hasAuthParamsInUrl as unknown as ReturnType<typeof vi.fn>).mockReturnValue(false); | ||
| const url = new URL("https://example.com/callback"); | ||
| const afterSignInUrl = "https://example.com/other"; | ||
| expect(hasAuthParams(url, afterSignInUrl)).toBe(false); | ||
| }); | ||
|
|
||
| // Edge case: trailing slash mismatch | ||
| it("normalizes URLs with trailing slashes", () => { | ||
| (hasAuthParamsInUrl as unknown as ReturnType<typeof vi.fn>).mockReturnValue(true); | ||
| const url = new URL("https://example.com/callback/"); | ||
| const afterSignInUrl = "https://example.com/callback"; | ||
| expect(hasAuthParams(url, afterSignInUrl)).toBe(false); // still false due to exact match | ||
| }); | ||
|
|
||
| // Edge case: relative afterSignInUrl | ||
| it("handles relative afterSignInUrl", () => { | ||
| (hasAuthParamsInUrl as unknown as ReturnType<typeof vi.fn>).mockReturnValue(true); | ||
| const url = new URL("https://example.com/callback"); | ||
| const afterSignInUrl = "/callback"; | ||
| expect(hasAuthParams(url, afterSignInUrl)).toBe(false); // relative URL fails exact match | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| /** | ||
| * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). | ||
| * | ||
| * WSO2 LLC. licenses this file to you under the Apache License, | ||
| * Version 2.0 (the "License"); you may not use this file except | ||
| * in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| import { renderHook, act } from '@testing-library/react'; | ||
| import { useForm, UseFormConfig } from '../hooks/useForm'; | ||
| import { vi, describe, it, expect } from 'vitest'; | ||
|
|
||
| interface LoginForm extends Record<string, string> { | ||
| username: string; | ||
| password: string; | ||
| email?: string; | ||
| } | ||
|
|
||
| describe('useForm hook - full coverage', () => { | ||
| const initialValues: LoginForm = { username: '', password: '', email: '' }; | ||
| const fields = [ | ||
| { name: 'username', required: true }, | ||
| { name: 'password', required: true }, | ||
| { name: 'email', required: false, validator: (value: string) => value.includes('@') ? null : 'Invalid email' } | ||
| ]; | ||
|
|
||
| const globalValidator = (values: LoginForm) => { | ||
| const errors: Record<string, string> = {}; | ||
| if (values.password && values.password.length < 6) { | ||
| errors['password'] = 'Password too short'; | ||
| } | ||
| return errors; | ||
| }; | ||
|
|
||
| const config: UseFormConfig<LoginForm> = { initialValues, fields, validator: globalValidator }; | ||
|
|
||
| it('initial state', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| expect(result.current.values).toEqual(initialValues); | ||
| expect(result.current.errors).toEqual({}); | ||
| expect(result.current.touched).toEqual({}); | ||
| expect(result.current.isValid).toBe(true); | ||
| expect(result.current.isSubmitted).toBe(false); | ||
| }); | ||
|
|
||
| it('setValue and validate on change', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>({ ...config, validateOnChange: true })); | ||
|
|
||
| act(() => result.current.setValue('username', 'Alice')); | ||
| expect(result.current.values.username).toBe('Alice'); | ||
|
|
||
| act(() => result.current.setValue('email', 'wrong-email')); | ||
| expect(result.current.errors['email']).toBe('Invalid email'); | ||
|
|
||
| act(() => result.current.setValue('email', 'alice@example.com')); | ||
| expect(result.current.errors['email']).toBeUndefined(); | ||
|
Check failure on line 65 in packages/react/src/__test__/useForm.test.ts
|
||
| }); | ||
|
|
||
| it('bulk setValues', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
|
|
||
| act(() => result.current.setValues({ username: 'Bob', password: '123456' })); | ||
| expect(result.current.values.username).toBe('Bob'); | ||
| expect(result.current.values.password).toBe('123456'); | ||
| }); | ||
|
|
||
| it('setTouched and validate on blur', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
|
|
||
| act(() => result.current.setTouched('username')); | ||
| expect(result.current.touched['username']).toBe(true); | ||
| expect(result.current.errors['username']).toBe('This field is required'); | ||
| }); | ||
|
|
||
| it('bulk setTouchedFields', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| act(() => result.current.setTouchedFields({ username: true, password: true })); | ||
| expect(result.current.touched['username']).toBe(true); | ||
| expect(result.current.touched['password']).toBe(true); | ||
| }); | ||
|
|
||
| it('touchAllFields triggers validation', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| act(() => result.current.touchAllFields()); | ||
| expect(result.current.touched['username']).toBe(true); | ||
| expect(result.current.touched['password']).toBe(true); | ||
| expect(result.current.errors['username']).toBe('This field is required'); | ||
| }); | ||
|
|
||
| it('setErrors and clearErrors', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| act(() => result.current.setErrors({ username: 'Custom error', password: 'Another error' })); | ||
| expect(result.current.errors['username']).toBe('Custom error'); | ||
| expect(result.current.errors['password']).toBe('Another error'); | ||
|
|
||
| act(() => result.current.clearErrors()); | ||
| expect(result.current.errors).toEqual({}); | ||
| }); | ||
|
|
||
| it('validateField returns correct errors', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| act(() => result.current.setValue('email', 'invalid')); | ||
| expect(result.current.validateField('email')).toBe('Invalid email'); | ||
| expect(result.current.validateField('username')).toBe('This field is required'); | ||
| }); | ||
|
|
||
| it('validateForm returns correct ValidationResult', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| act(() => result.current.setValue('password', '123')); | ||
| const validation = result.current.validateForm(); | ||
| expect(validation.isValid).toBe(false); | ||
| expect(validation.errors['password']).toBe('Password too short'); | ||
| }); | ||
|
|
||
| it('handleSubmit prevents submission if invalid', async () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| const onSubmit = vi.fn(); | ||
|
|
||
| await act(async () => { | ||
| await result.current.handleSubmit(onSubmit)({ preventDefault: vi.fn() } as any); | ||
| }); | ||
|
|
||
| expect(onSubmit).not.toHaveBeenCalled(); | ||
| expect(result.current.isSubmitted).toBe(true); | ||
| }); | ||
|
|
||
| it('handleSubmit calls onSubmit if valid', async () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| const onSubmit = vi.fn(); | ||
|
|
||
| act(() => { | ||
| result.current.setValues({ username: 'Alice', password: '123456', email: 'alice@example.com' }); | ||
| }); | ||
|
|
||
| await act(async () => { | ||
| await result.current.handleSubmit(onSubmit)(); | ||
| }); | ||
|
|
||
| expect(onSubmit).toHaveBeenCalledWith({ username: 'Alice', password: '123456', email: 'alice@example.com' }); | ||
| }); | ||
|
|
||
| it('reset restores initial state', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| act(() => { | ||
| result.current.setValue('username', 'Changed'); | ||
| result.current.setTouched('username'); | ||
| result.current.setError('username', 'Error'); | ||
| result.current.reset(); | ||
| }); | ||
|
|
||
| expect(result.current.values).toEqual(initialValues); | ||
| expect(result.current.touched).toEqual({}); | ||
| expect(result.current.errors).toEqual({}); | ||
| expect(result.current.isSubmitted).toBe(false); | ||
| }); | ||
|
|
||
| it('getFieldProps works correctly', () => { | ||
| const { result } = renderHook(() => useForm<LoginForm>(config)); | ||
| const props = result.current.getFieldProps('username'); | ||
|
|
||
| expect(props.name).toBe('username'); | ||
| expect(props.required).toBe(true); | ||
| expect(props.value).toBe(''); | ||
| expect(props.touched).toBe(false); | ||
| expect(props.error).toBeUndefined(); | ||
| expect(typeof props.onBlur).toBe('function'); | ||
| expect(typeof props.onChange).toBe('function'); | ||
|
|
||
| act(() => props.onChange('NewValue')); | ||
| expect(result.current.values.username).toBe('NewValue'); | ||
|
|
||
| act(() => props.onBlur()); | ||
| expect(result.current.touched['username']).toBe(true); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check the indentation of these files.
We use
2spaces.I hope you have read the
CONTRIBUTINGguideline and installed the necessary plugins: https://github.com/brionmario/javascript/blob/next/CONTRIBUTING.md#development-tools