test: update test coverage UI component Joi (#3707)

* test: update test coverage joi

* test: update test export all components

* test: update clear mock useOs

* test: remove delete global window during test case getInitialValue

* test: update getValueInEffect with mock userAgent
This commit is contained in:
Faisal Amir 2024-09-23 12:46:50 +07:00 committed by GitHub
parent 3091bb0e5e
commit 5b7f0c1308
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 224 additions and 55 deletions

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useRef } from 'react'
const DEFAULT_EVENTS = ['mousedown', 'touchstart']
@ -8,34 +7,43 @@ export function useClickOutside<T extends HTMLElement = any>(
events?: string[] | null,
nodes?: (HTMLElement | null)[]
) {
const ref = useRef<T>()
const ref = useRef<T>(null)
useEffect(() => {
const listener = (event: any) => {
const { target } = event ?? {}
const listener = (event: Event) => {
const target = event.target as HTMLElement
// Check if the target or any ancestor has the data-ignore-outside-clicks attribute
const shouldIgnore =
target.closest('[data-ignore-outside-clicks]') !== null
if (Array.isArray(nodes)) {
const shouldIgnore =
target?.hasAttribute('data-ignore-outside-clicks') ||
(!document.body.contains(target) && target.tagName !== 'HTML')
const shouldTrigger = nodes.every(
(node) => !!node && !event.composedPath().includes(node)
)
shouldTrigger && !shouldIgnore && handler()
} else if (ref.current && !ref.current.contains(target)) {
if (shouldTrigger && !shouldIgnore) {
handler()
}
} else if (
ref.current &&
!ref.current.contains(target) &&
!shouldIgnore
) {
handler()
}
}
;(events || DEFAULT_EVENTS).forEach((fn) =>
document.addEventListener(fn, listener)
const eventList = events || DEFAULT_EVENTS
eventList.forEach((event) =>
document.documentElement.addEventListener(event, listener)
)
return () => {
;(events || DEFAULT_EVENTS).forEach((fn) =>
document.removeEventListener(fn, listener)
eventList.forEach((event) =>
document.documentElement.removeEventListener(event, listener)
)
}
}, [ref, handler, nodes])
}, [handler, nodes, events])
return ref
}

View File

@ -1,55 +1,84 @@
import React from 'react'
import { render, fireEvent, act } from '@testing-library/react'
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
import { useClickOutside } from './index'
// Mock component to test the hook
const TestComponent: React.FC<{ onClickOutside: () => void }> = ({
onClickOutside,
const TestComponent = ({
handler,
nodes,
}: {
handler: () => void
nodes?: (HTMLElement | null)[]
}) => {
const ref = useClickOutside(onClickOutside)
return <div ref={ref as React.RefObject<HTMLDivElement>}>Test</div>
const ref = useClickOutside(handler, undefined, nodes)
return (
<div ref={ref} data-testid="clickable">
Click me
</div>
)
}
describe('@joi/hooks/useClickOutside', () => {
it('should call handler when clicking outside', () => {
const handleClickOutside = jest.fn()
const { container } = render(
<TestComponent onClickOutside={handleClickOutside} />
)
describe('useClickOutside', () => {
afterEach(cleanup)
act(() => {
fireEvent.mouseDown(document.body)
})
it('should call handler when clicking outside the element', () => {
const handler = jest.fn()
render(<TestComponent handler={handler} />)
expect(handleClickOutside).toHaveBeenCalledTimes(1)
fireEvent.mouseDown(document.body)
expect(handler).toHaveBeenCalledTimes(1)
})
it('should not call handler when clicking inside', () => {
const handleClickOutside = jest.fn()
const { getByText } = render(
<TestComponent onClickOutside={handleClickOutside} />
)
it('should not call handler when clicking inside the element', () => {
const handler = jest.fn()
render(<TestComponent handler={handler} />)
act(() => {
fireEvent.mouseDown(getByText('Test'))
})
expect(handleClickOutside).not.toHaveBeenCalled()
fireEvent.mouseDown(screen.getByTestId('clickable'))
expect(handler).not.toHaveBeenCalled()
})
it('should work with custom events', () => {
const handleClickOutside = jest.fn()
const TestComponentWithCustomEvent: React.FC = () => {
const ref = useClickOutside(handleClickOutside, ['click'])
return <div ref={ref as React.RefObject<HTMLDivElement>}>Test</div>
}
it('should not call handler if target has data-ignore-outside-clicks attribute', () => {
const handler = jest.fn()
render(
<>
<TestComponent handler={handler} />
<div data-ignore-outside-clicks>Ignore this</div>
</>
)
render(<TestComponentWithCustomEvent />)
// Ensure that the div with the attribute is correctly queried
fireEvent.mouseDown(screen.getByText('Ignore this'))
expect(handler).not.toHaveBeenCalled()
})
act(() => {
fireEvent.click(document.body)
})
it('should call handler when clicking outside if nodes is an empty array', () => {
const handler = jest.fn()
render(<TestComponent handler={handler} nodes={[]} />)
expect(handleClickOutside).toHaveBeenCalledTimes(1)
fireEvent.mouseDown(document.body)
expect(handler).toHaveBeenCalledTimes(1)
})
it('should not call handler if clicking inside nodes', () => {
const handler = jest.fn()
const node = document.createElement('div')
document.body.appendChild(node)
render(
<>
<TestComponent handler={handler} nodes={[node]} />
</>
)
fireEvent.mouseDown(node)
expect(handler).not.toHaveBeenCalled()
})
it('should call handler if nodes is undefined', () => {
const handler = jest.fn()
render(<TestComponent handler={handler} nodes={undefined} />)
fireEvent.mouseDown(document.body)
expect(handler).toHaveBeenCalledTimes(1)
})
})

View File

@ -23,7 +23,7 @@ function attachMediaListener(
}
}
function getInitialValue(query: string, initialValue?: boolean) {
export function getInitialValue(query: string, initialValue?: boolean) {
if (typeof initialValue === 'boolean') {
return initialValue
}

View File

@ -1,5 +1,8 @@
import { renderHook, act } from '@testing-library/react'
import { useMediaQuery } from './index'
import { useMediaQuery, getInitialValue } from './index'
const global = globalThis
const originalWindow = global.window
describe('@joi/hooks/useMediaQuery', () => {
const matchMediaMock = jest.fn()
@ -10,6 +13,39 @@ describe('@joi/hooks/useMediaQuery', () => {
afterEach(() => {
matchMediaMock.mockClear()
global.window = originalWindow
})
it('should return undetermined when window is undefined', () => {
delete (global as any).window
expect(getInitialValue('(max-width: 600px)', true)).toBe(true)
expect(getInitialValue('(max-width: 600px)', false)).toBe(false)
})
it('should return default return false', () => {
delete (global as any).window
expect(getInitialValue('(max-width: 600px)')).toBe(false)
})
it('should return matchMedia result when window is defined and matchMedia exists', () => {
// Mock window.matchMedia
const matchMediaMock = jest.fn().mockImplementation((query) => ({
matches: query === '(max-width: 600px)',
media: query,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
}))
// Mock window and matchMedia
;(global as any).window = { matchMedia: matchMediaMock }
// Test the function behavior
expect(getInitialValue('(max-width: 600px)')).toBe(true) // Query should match
expect(matchMediaMock).toHaveBeenCalledWith('(max-width: 600px)')
// Test with a non-matching query
expect(getInitialValue('(min-width: 1200px)')).toBe(false) // Query should not match
expect(matchMediaMock).toHaveBeenCalledWith('(min-width: 1200px)')
})
it('should return initial value when getInitialValueInEffect is true', () => {
@ -87,4 +123,38 @@ describe('@joi/hooks/useMediaQuery', () => {
expect(result.current).toBe(true)
})
it('should return undefined when matchMedia is not available', () => {
delete (global as any).window.matchMedia
const { result } = renderHook(() => useMediaQuery('(max-width: 600px)'))
expect(result.current).toBe(undefined)
})
it('should use initialValue when getInitialValueInEffect is true', () => {
const { result } = renderHook(() =>
useMediaQuery('(max-width: 600px)', true, {
getInitialValueInEffect: true,
})
)
expect(result.current).toBe(true)
})
it('should use getInitialValue when getInitialValueInEffect is false', () => {
const { result } = renderHook(() =>
useMediaQuery('(max-width: 600px)', undefined, {
getInitialValueInEffect: false,
})
)
expect(result.current).toBe(false)
})
it('should use initialValue as false when getInitialValueInEffect is true', () => {
const { result } = renderHook(() =>
useMediaQuery('(max-width: 600px)', false, {
getInitialValueInEffect: true,
})
)
expect(result.current).toBe(false)
})
})

View File

@ -8,7 +8,7 @@ export type OS =
| 'android'
| 'linux'
function getOS(): OS {
export function getOS(): OS {
if (typeof window === 'undefined') {
return 'undetermined'
}

View File

@ -1,5 +1,6 @@
import { renderHook } from '@testing-library/react'
import { useOs } from './index'
import { useOs, getOS } from './index'
import '@testing-library/jest-dom'
const platforms = {
windows: [
@ -21,10 +22,28 @@ const platforms = {
} as const
describe('@joi/hooks/useOS', () => {
const global = globalThis
const originalWindow = global.window
afterEach(() => {
global.window = originalWindow
jest.clearAllMocks()
})
it('should return undetermined when window is undefined', () => {
delete (global as any).window
expect(getOS()).toBe('undetermined')
})
it('should return undetermined when getValueInEffect is false', () => {
jest
.spyOn(window.navigator, 'userAgent', 'get')
.mockReturnValueOnce('UNKNOWN_USER_AGENT')
const { result } = renderHook(() => useOs({ getValueInEffect: false }))
expect(result.current).toBe('undetermined')
})
Object.entries(platforms).forEach(([os, userAgents]) => {
it.each(userAgents)(`should detect %s platform on ${os}`, (userAgent) => {
jest

43
joi/src/index.test.ts Normal file
View File

@ -0,0 +1,43 @@
import * as components from './index'
// Mock styles globally for all components in this test
jest.mock('./core/Tooltip/styles.scss', () => ({}))
jest.mock('./core/ScrollArea/styles.scss', () => ({}))
jest.mock('./core/Button/styles.scss', () => ({}))
jest.mock('./core/Switch/styles.scss', () => ({}))
jest.mock('./core/Progress/styles.scss', () => ({}))
jest.mock('./core/Checkbox/styles.scss', () => ({}))
jest.mock('./core/Badge/styles.scss', () => ({}))
jest.mock('./core/Modal/styles.scss', () => ({}))
jest.mock('./core/Slider/styles.scss', () => ({}))
jest.mock('./core/Input/styles.scss', () => ({}))
jest.mock('./core/Select/styles.scss', () => ({}))
jest.mock('./core/TextArea/styles.scss', () => ({}))
jest.mock('./core/Tabs/styles.scss', () => ({}))
jest.mock('./core/Accordion/styles.scss', () => ({}))
describe('Exports', () => {
it('exports all components and hooks', () => {
expect(components.Tooltip).toBeDefined()
expect(components.ScrollArea).toBeDefined()
expect(components.Button).toBeDefined()
expect(components.Switch).toBeDefined()
expect(components.Progress).toBeDefined()
expect(components.Checkbox).toBeDefined()
expect(components.Badge).toBeDefined()
expect(components.Modal).toBeDefined()
expect(components.Slider).toBeDefined()
expect(components.Input).toBeDefined()
expect(components.Select).toBeDefined()
expect(components.TextArea).toBeDefined()
expect(components.Tabs).toBeDefined()
expect(components.Accordion).toBeDefined()
expect(components.useClipboard).toBeDefined()
expect(components.usePageLeave).toBeDefined()
expect(components.useTextSelection).toBeDefined()
expect(components.useClickOutside).toBeDefined()
expect(components.useOs).toBeDefined()
expect(components.useMediaQuery).toBeDefined()
})
})