preact-missing-hooks 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/sync-npm.yml +38 -0
- package/.github/workflows/test-hooks.yml +4 -1
- package/Readme.md +165 -51
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/useClipboard.d.ts +36 -0
- package/dist/useNetworkState.d.ts +40 -0
- package/dist/usePreferredTheme.d.ts +21 -0
- package/dist/useWrappedChildren.d.ts +10 -0
- package/package.json +64 -58
- package/src/index.ts +5 -1
- package/src/useClipboard.ts +97 -0
- package/src/useNetworkState.ts +122 -0
- package/src/usePreferredTheme.ts +68 -0
- package/src/useWrappedChildren.ts +58 -0
- package/tests/useClipboard.test.tsx +159 -0
- package/tests/useNetworkState.test.tsx +140 -0
- package/tests/usePreferredTheme.test.tsx +119 -0
- package/tests/useWrappedChildren.test.tsx +156 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h } from 'preact'
|
|
3
|
+
import { useState } from 'preact/hooks'
|
|
4
|
+
import { render, fireEvent, waitFor } from '@testing-library/preact'
|
|
5
|
+
import { useClipboard } from '../src/useClipboard'
|
|
6
|
+
|
|
7
|
+
describe('useClipboard', () => {
|
|
8
|
+
const originalNavigator = global.navigator
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
Object.defineProperty(global, 'navigator', {
|
|
12
|
+
value: originalNavigator,
|
|
13
|
+
writable: true,
|
|
14
|
+
})
|
|
15
|
+
vi.useRealTimers()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('copy succeeds and sets copied to true', async () => {
|
|
19
|
+
vi.useFakeTimers()
|
|
20
|
+
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
21
|
+
Object.defineProperty(global, 'navigator', {
|
|
22
|
+
value: { ...originalNavigator, clipboard: { writeText } },
|
|
23
|
+
writable: true,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function TestComponent() {
|
|
27
|
+
const { copy, copied } = useClipboard({ resetDelay: 1000 })
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
<button onClick={() => copy('test')}>Copy</button>
|
|
31
|
+
<span data-testid="copied">{String(copied)}</span>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { getByText, getByTestId } = render(<TestComponent />)
|
|
37
|
+
expect(getByTestId('copied').textContent).toBe('false')
|
|
38
|
+
|
|
39
|
+
fireEvent.click(getByText('Copy'))
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(writeText).toHaveBeenCalledWith('test')
|
|
42
|
+
})
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(getByTestId('copied').textContent).toBe('true')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
vi.advanceTimersByTime(1000)
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(getByTestId('copied').textContent).toBe('false')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('copy returns false and sets error when clipboard API fails', async () => {
|
|
54
|
+
const writeText = vi.fn().mockRejectedValue(new Error('Permission denied'))
|
|
55
|
+
Object.defineProperty(global, 'navigator', {
|
|
56
|
+
value: { ...originalNavigator, clipboard: { writeText } },
|
|
57
|
+
writable: true,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
function TestComponent() {
|
|
61
|
+
const { copy, error } = useClipboard()
|
|
62
|
+
return (
|
|
63
|
+
<div>
|
|
64
|
+
<button onClick={() => copy('test')}>Copy</button>
|
|
65
|
+
<span data-testid="error">{error?.message ?? 'none'}</span>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { getByText, getByTestId } = render(<TestComponent />)
|
|
71
|
+
fireEvent.click(getByText('Copy'))
|
|
72
|
+
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
expect(getByTestId('error').textContent).toBe('Permission denied')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('paste returns text when clipboard API succeeds', async () => {
|
|
79
|
+
const readText = vi.fn().mockResolvedValue('pasted content')
|
|
80
|
+
Object.defineProperty(global, 'navigator', {
|
|
81
|
+
value: { ...originalNavigator, clipboard: { writeText: vi.fn(), readText } },
|
|
82
|
+
writable: true,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
function TestComponent() {
|
|
86
|
+
const [text, setText] = useState('')
|
|
87
|
+
const { paste } = useClipboard()
|
|
88
|
+
const handlePaste = async () => {
|
|
89
|
+
const result = await paste()
|
|
90
|
+
setText(result)
|
|
91
|
+
}
|
|
92
|
+
return (
|
|
93
|
+
<div>
|
|
94
|
+
<button onClick={handlePaste}>Paste</button>
|
|
95
|
+
<span data-testid="text">{text}</span>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { getByText, getByTestId } = render(<TestComponent />)
|
|
101
|
+
|
|
102
|
+
fireEvent.click(getByText('Paste'))
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(readText).toHaveBeenCalled()
|
|
105
|
+
})
|
|
106
|
+
await waitFor(() => {
|
|
107
|
+
expect(getByTestId('text').textContent).toBe('pasted content')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('reset clears copied and error state', async () => {
|
|
112
|
+
vi.useFakeTimers()
|
|
113
|
+
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
114
|
+
Object.defineProperty(global, 'navigator', {
|
|
115
|
+
value: { ...originalNavigator, clipboard: { writeText } },
|
|
116
|
+
writable: true,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
function TestComponent() {
|
|
120
|
+
const { copy, copied, reset } = useClipboard({ resetDelay: 5000 })
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
<button onClick={() => copy('test')}>Copy</button>
|
|
124
|
+
<button onClick={reset}>Reset</button>
|
|
125
|
+
<span data-testid="copied">{String(copied)}</span>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { getByText, getByTestId } = render(<TestComponent />)
|
|
131
|
+
fireEvent.click(getByText('Copy'))
|
|
132
|
+
await waitFor(() => expect(getByTestId('copied').textContent).toBe('true'))
|
|
133
|
+
fireEvent.click(getByText('Reset'))
|
|
134
|
+
expect(getByTestId('copied').textContent).toBe('false')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('handles missing clipboard API', async () => {
|
|
138
|
+
Object.defineProperty(global, 'navigator', {
|
|
139
|
+
value: { ...originalNavigator, clipboard: undefined },
|
|
140
|
+
writable: true,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
function TestComponent() {
|
|
144
|
+
const { copy, error } = useClipboard()
|
|
145
|
+
return (
|
|
146
|
+
<div>
|
|
147
|
+
<button onClick={() => copy('test')}>Copy</button>
|
|
148
|
+
<span data-testid="error">{error?.message ?? 'none'}</span>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { getByText, getByTestId } = render(<TestComponent />)
|
|
154
|
+
fireEvent.click(getByText('Copy'))
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(getByTestId('error').textContent).toBe('Clipboard API is not available')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h } from 'preact'
|
|
3
|
+
import { render, waitFor } from '@testing-library/preact'
|
|
4
|
+
import { useNetworkState } from '../src/useNetworkState'
|
|
5
|
+
|
|
6
|
+
describe('useNetworkState', () => {
|
|
7
|
+
const originalNavigator = global.navigator
|
|
8
|
+
const originalAddEventListener = window.addEventListener
|
|
9
|
+
const originalRemoveEventListener = window.removeEventListener
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
Object.defineProperty(global, 'navigator', {
|
|
13
|
+
value: originalNavigator,
|
|
14
|
+
writable: true,
|
|
15
|
+
})
|
|
16
|
+
window.addEventListener = originalAddEventListener
|
|
17
|
+
window.removeEventListener = originalRemoveEventListener
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns online: true when navigator.onLine is true', () => {
|
|
21
|
+
Object.defineProperty(global, 'navigator', {
|
|
22
|
+
value: { ...originalNavigator, onLine: true },
|
|
23
|
+
writable: true,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function TestComponent() {
|
|
27
|
+
const state = useNetworkState()
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
<span data-testid="online">{String(state.online)}</span>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { getByTestId } = render(<TestComponent />)
|
|
36
|
+
expect(getByTestId('online').textContent).toBe('true')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns online: false when navigator.onLine is false', () => {
|
|
40
|
+
Object.defineProperty(global, 'navigator', {
|
|
41
|
+
value: { ...originalNavigator, onLine: false },
|
|
42
|
+
writable: true,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
function TestComponent() {
|
|
46
|
+
const state = useNetworkState()
|
|
47
|
+
return (
|
|
48
|
+
<div>
|
|
49
|
+
<span data-testid="online">{String(state.online)}</span>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { getByTestId } = render(<TestComponent />)
|
|
55
|
+
expect(getByTestId('online').textContent).toBe('false')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('includes connection info when navigator.connection is available', () => {
|
|
59
|
+
const mockConnection = {
|
|
60
|
+
effectiveType: '4g',
|
|
61
|
+
downlink: 10,
|
|
62
|
+
rtt: 50,
|
|
63
|
+
saveData: false,
|
|
64
|
+
type: 'wifi',
|
|
65
|
+
addEventListener: vi.fn(),
|
|
66
|
+
removeEventListener: vi.fn(),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Object.defineProperty(global, 'navigator', {
|
|
70
|
+
value: {
|
|
71
|
+
...originalNavigator,
|
|
72
|
+
onLine: true,
|
|
73
|
+
connection: mockConnection,
|
|
74
|
+
},
|
|
75
|
+
writable: true,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
function TestComponent() {
|
|
79
|
+
const state = useNetworkState()
|
|
80
|
+
return (
|
|
81
|
+
<div>
|
|
82
|
+
<span data-testid="effectiveType">{state.effectiveType ?? 'none'}</span>
|
|
83
|
+
<span data-testid="downlink">{state.downlink ?? 'none'}</span>
|
|
84
|
+
<span data-testid="rtt">{state.rtt ?? 'none'}</span>
|
|
85
|
+
<span data-testid="saveData">{String(state.saveData ?? 'none')}</span>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { getByTestId } = render(<TestComponent />)
|
|
91
|
+
expect(getByTestId('effectiveType').textContent).toBe('4g')
|
|
92
|
+
expect(getByTestId('downlink').textContent).toBe('10')
|
|
93
|
+
expect(getByTestId('rtt').textContent).toBe('50')
|
|
94
|
+
expect(getByTestId('saveData').textContent).toBe('false')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('updates when online/offline events fire', async () => {
|
|
98
|
+
let onlineHandler: () => void = () => { }
|
|
99
|
+
let offlineHandler: () => void = () => { }
|
|
100
|
+
|
|
101
|
+
window.addEventListener = vi.fn((event: string, handler: () => void) => {
|
|
102
|
+
if (event === 'online') onlineHandler = handler
|
|
103
|
+
if (event === 'offline') offlineHandler = handler
|
|
104
|
+
}) as any
|
|
105
|
+
window.removeEventListener = vi.fn()
|
|
106
|
+
|
|
107
|
+
Object.defineProperty(global, 'navigator', {
|
|
108
|
+
value: { ...originalNavigator, onLine: true },
|
|
109
|
+
writable: true,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
function TestComponent() {
|
|
113
|
+
const state = useNetworkState()
|
|
114
|
+
return <span data-testid="online">{String(state.online)}</span>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { getByTestId } = render(<TestComponent />)
|
|
118
|
+
expect(getByTestId('online').textContent).toBe('true')
|
|
119
|
+
|
|
120
|
+
Object.defineProperty(global, 'navigator', {
|
|
121
|
+
value: { ...originalNavigator, onLine: false },
|
|
122
|
+
writable: true,
|
|
123
|
+
})
|
|
124
|
+
offlineHandler()
|
|
125
|
+
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(getByTestId('online').textContent).toBe('false')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
Object.defineProperty(global, 'navigator', {
|
|
131
|
+
value: { ...originalNavigator, onLine: true },
|
|
132
|
+
writable: true,
|
|
133
|
+
})
|
|
134
|
+
onlineHandler()
|
|
135
|
+
|
|
136
|
+
await waitFor(() => {
|
|
137
|
+
expect(getByTestId('online').textContent).toBe('true')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h } from 'preact'
|
|
3
|
+
import { render, waitFor } from '@testing-library/preact'
|
|
4
|
+
import { usePreferredTheme } from '../src/usePreferredTheme'
|
|
5
|
+
|
|
6
|
+
describe('usePreferredTheme', () => {
|
|
7
|
+
const originalMatchMedia = window.matchMedia
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
window.matchMedia = originalMatchMedia
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function createMockMediaQuery(matches: boolean) {
|
|
14
|
+
return {
|
|
15
|
+
matches,
|
|
16
|
+
media: '(prefers-color-scheme: dark)',
|
|
17
|
+
onchange: null as ((e: MediaQueryListEvent) => void) | null,
|
|
18
|
+
addEventListener: vi.fn((_event: string, handler: (e: MediaQueryListEvent) => void) => {
|
|
19
|
+
; (createMockMediaQuery as any)._handler = handler
|
|
20
|
+
}),
|
|
21
|
+
removeEventListener: vi.fn(),
|
|
22
|
+
addListener: vi.fn(),
|
|
23
|
+
removeListener: vi.fn(),
|
|
24
|
+
dispatchEvent: vi.fn(),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
it('returns "dark" when prefers-color-scheme: dark matches', () => {
|
|
29
|
+
const darkQuery = createMockMediaQuery(true)
|
|
30
|
+
const lightQuery = createMockMediaQuery(false)
|
|
31
|
+
|
|
32
|
+
window.matchMedia = vi.fn((query: string) => {
|
|
33
|
+
if (query.includes('dark')) return darkQuery
|
|
34
|
+
return lightQuery
|
|
35
|
+
}) as any
|
|
36
|
+
|
|
37
|
+
function TestComponent() {
|
|
38
|
+
const theme = usePreferredTheme()
|
|
39
|
+
return <div data-testid="theme">{theme}</div>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { getByTestId } = render(<TestComponent />)
|
|
43
|
+
expect(getByTestId('theme').textContent).toBe('dark')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns "light" when prefers-color-scheme: light matches', () => {
|
|
47
|
+
const darkQuery = createMockMediaQuery(false)
|
|
48
|
+
const lightQuery = createMockMediaQuery(true)
|
|
49
|
+
|
|
50
|
+
window.matchMedia = vi.fn((query: string) => {
|
|
51
|
+
if (query.includes('dark')) return darkQuery
|
|
52
|
+
return lightQuery
|
|
53
|
+
}) as any
|
|
54
|
+
|
|
55
|
+
function TestComponent() {
|
|
56
|
+
const theme = usePreferredTheme()
|
|
57
|
+
return <div data-testid="theme">{theme}</div>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { getByTestId } = render(<TestComponent />)
|
|
61
|
+
expect(getByTestId('theme').textContent).toBe('light')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns "no-preference" when neither dark nor light matches', () => {
|
|
65
|
+
const darkQuery = createMockMediaQuery(false)
|
|
66
|
+
const lightQuery = createMockMediaQuery(false)
|
|
67
|
+
|
|
68
|
+
window.matchMedia = vi.fn((query: string) => {
|
|
69
|
+
if (query.includes('dark')) return darkQuery
|
|
70
|
+
return lightQuery
|
|
71
|
+
}) as any
|
|
72
|
+
|
|
73
|
+
function TestComponent() {
|
|
74
|
+
const theme = usePreferredTheme()
|
|
75
|
+
return <div data-testid="theme">{theme}</div>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { getByTestId } = render(<TestComponent />)
|
|
79
|
+
expect(getByTestId('theme').textContent).toBe('no-preference')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('updates when media query change event fires', async () => {
|
|
83
|
+
let changeHandler: ((e: MediaQueryListEvent) => void) | null = null
|
|
84
|
+
const darkQuery = {
|
|
85
|
+
matches: true,
|
|
86
|
+
media: '(prefers-color-scheme: dark)',
|
|
87
|
+
onchange: null,
|
|
88
|
+
addEventListener: vi.fn((_event: string, handler: (e: MediaQueryListEvent) => void) => {
|
|
89
|
+
changeHandler = handler
|
|
90
|
+
}),
|
|
91
|
+
removeEventListener: vi.fn(),
|
|
92
|
+
addListener: vi.fn(),
|
|
93
|
+
removeListener: vi.fn(),
|
|
94
|
+
dispatchEvent: vi.fn(),
|
|
95
|
+
}
|
|
96
|
+
const lightQuery = createMockMediaQuery(false)
|
|
97
|
+
|
|
98
|
+
window.matchMedia = vi.fn((query: string) => {
|
|
99
|
+
if (query.includes('dark')) return darkQuery
|
|
100
|
+
return lightQuery
|
|
101
|
+
}) as any
|
|
102
|
+
|
|
103
|
+
function TestComponent() {
|
|
104
|
+
const theme = usePreferredTheme()
|
|
105
|
+
return <div data-testid="theme">{theme}</div>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { getByTestId } = render(<TestComponent />)
|
|
109
|
+
expect(getByTestId('theme').textContent).toBe('dark')
|
|
110
|
+
|
|
111
|
+
// Simulate user switching to light mode
|
|
112
|
+
; (darkQuery as any).matches = false
|
|
113
|
+
changeHandler?.({ matches: false, media: '(prefers-color-scheme: dark)' } as MediaQueryListEvent)
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(getByTestId('theme').textContent).toBe('light')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
import { h, ComponentChildren } from 'preact'
|
|
3
|
+
import { render, screen } from '@testing-library/preact'
|
|
4
|
+
import '@testing-library/jest-dom'
|
|
5
|
+
import { useWrappedChildren } from '../src/useWrappedChildren'
|
|
6
|
+
|
|
7
|
+
interface TestChildProps {
|
|
8
|
+
className?: string
|
|
9
|
+
'data-testid'?: string
|
|
10
|
+
style?: Record<string, string>
|
|
11
|
+
children?: ComponentChildren
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function TestChild({ className, 'data-testid': testId, style, children }: TestChildProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={className} data-testid={testId} style={style}>
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ParentWithWrappedChildren({
|
|
23
|
+
children,
|
|
24
|
+
injectProps,
|
|
25
|
+
mergeStrategy
|
|
26
|
+
}: {
|
|
27
|
+
children: ComponentChildren
|
|
28
|
+
injectProps: Record<string, any>
|
|
29
|
+
mergeStrategy?: 'override' | 'preserve'
|
|
30
|
+
}) {
|
|
31
|
+
const wrappedChildren = useWrappedChildren(children, injectProps, mergeStrategy)
|
|
32
|
+
return <div data-testid="parent">{wrappedChildren}</div>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('useWrappedChildren', () => {
|
|
36
|
+
it('injects props into single child component', () => {
|
|
37
|
+
const injectProps = { className: 'injected', 'data-testid': 'child' }
|
|
38
|
+
|
|
39
|
+
render(
|
|
40
|
+
<ParentWithWrappedChildren injectProps={injectProps}>
|
|
41
|
+
<TestChild>Original content</TestChild>
|
|
42
|
+
</ParentWithWrappedChildren>
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const child = screen.getByTestId('child')
|
|
46
|
+
expect(child).toHaveClass('injected')
|
|
47
|
+
expect(child).toHaveTextContent('Original content')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('injects props into multiple child components', () => {
|
|
51
|
+
const injectProps = { className: 'injected' }
|
|
52
|
+
|
|
53
|
+
render(
|
|
54
|
+
<ParentWithWrappedChildren injectProps={injectProps}>
|
|
55
|
+
<TestChild data-testid="child1">First child</TestChild>
|
|
56
|
+
<TestChild data-testid="child2">Second child</TestChild>
|
|
57
|
+
</ParentWithWrappedChildren>
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const child1 = screen.getByTestId('child1')
|
|
61
|
+
const child2 = screen.getByTestId('child2')
|
|
62
|
+
|
|
63
|
+
expect(child1).toHaveClass('injected')
|
|
64
|
+
expect(child2).toHaveClass('injected')
|
|
65
|
+
expect(child1).toHaveTextContent('First child')
|
|
66
|
+
expect(child2).toHaveTextContent('Second child')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('preserves existing props by default', () => {
|
|
70
|
+
const injectProps = { className: 'injected', style: { color: 'blue' } }
|
|
71
|
+
|
|
72
|
+
render(
|
|
73
|
+
<ParentWithWrappedChildren injectProps={injectProps}>
|
|
74
|
+
<TestChild
|
|
75
|
+
className="existing"
|
|
76
|
+
data-testid="child"
|
|
77
|
+
style={{ backgroundColor: 'red' }}
|
|
78
|
+
>
|
|
79
|
+
Content
|
|
80
|
+
</TestChild>
|
|
81
|
+
</ParentWithWrappedChildren>
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const child = screen.getByTestId('child')
|
|
85
|
+
expect(child).toHaveClass('existing') // existing prop preserved
|
|
86
|
+
expect(child).not.toHaveClass('injected') // injected prop not applied due to conflict
|
|
87
|
+
|
|
88
|
+
// Check computed styles
|
|
89
|
+
const computedStyle = getComputedStyle(child)
|
|
90
|
+
expect(computedStyle.backgroundColor).toBe('rgb(255, 0, 0)') // red
|
|
91
|
+
expect(computedStyle.color).toBe('rgb(0, 0, 255)') // blue
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('overrides existing props when mergeStrategy is override', () => {
|
|
95
|
+
const injectProps = { className: 'injected', 'data-testid': 'child' }
|
|
96
|
+
|
|
97
|
+
render(
|
|
98
|
+
<ParentWithWrappedChildren
|
|
99
|
+
injectProps={injectProps}
|
|
100
|
+
mergeStrategy="override"
|
|
101
|
+
>
|
|
102
|
+
<TestChild className="existing">Content</TestChild>
|
|
103
|
+
</ParentWithWrappedChildren>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const child = screen.getByTestId('child')
|
|
107
|
+
expect(child).toHaveClass('injected') // injected prop applied
|
|
108
|
+
expect(child).not.toHaveClass('existing') // existing prop overridden
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('handles non-element children gracefully', () => {
|
|
112
|
+
const injectProps = { className: 'injected' }
|
|
113
|
+
|
|
114
|
+
render(
|
|
115
|
+
<ParentWithWrappedChildren injectProps={injectProps}>
|
|
116
|
+
Plain text content
|
|
117
|
+
<TestChild data-testid="child">Element content</TestChild>
|
|
118
|
+
{42}
|
|
119
|
+
{null}
|
|
120
|
+
</ParentWithWrappedChildren>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const parent = screen.getByTestId('parent')
|
|
124
|
+
const child = screen.getByTestId('child')
|
|
125
|
+
|
|
126
|
+
expect(parent).toHaveTextContent('Plain text content')
|
|
127
|
+
expect(parent).toHaveTextContent('42')
|
|
128
|
+
expect(child).toHaveClass('injected')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('returns undefined children unchanged', () => {
|
|
132
|
+
const injectProps = { className: 'injected' }
|
|
133
|
+
|
|
134
|
+
render(
|
|
135
|
+
<ParentWithWrappedChildren injectProps={injectProps}>
|
|
136
|
+
{undefined}
|
|
137
|
+
</ParentWithWrappedChildren>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const parent = screen.getByTestId('parent')
|
|
141
|
+
expect(parent).toBeEmptyDOMElement()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('returns null children unchanged', () => {
|
|
145
|
+
const injectProps = { className: 'injected' }
|
|
146
|
+
|
|
147
|
+
render(
|
|
148
|
+
<ParentWithWrappedChildren injectProps={injectProps}>
|
|
149
|
+
{null}
|
|
150
|
+
</ParentWithWrappedChildren>
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const parent = screen.getByTestId('parent')
|
|
154
|
+
expect(parent).toBeEmptyDOMElement()
|
|
155
|
+
})
|
|
156
|
+
})
|