Skip to content

Commit 2ecaabd

Browse files
committed
test: add regression test for MeoNodeUnmounter to forward implicit props in MUI RadioGroup integration
Added a test to ensure MeoNodeUnmounter correctly forwards props injected via React.cloneElement, addressing issues with libraries like MUI where RadioGroup injects 'checked' and 'onChange' into Radio components. This prevents swallowing of props and verifies proper behavior of controlled radio inputs. Also updated an existing cache size assertion to allow equality, reflecting improved mount tracking.
1 parent b345ec0 commit 2ecaabd

File tree

1 file changed

+71
-5
lines changed

1 file changed

+71
-5
lines changed

tests/memoization.test.ts

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { jest } from '@jest/globals'
2-
import { Div, H1, Node, P, Span } from '@src/main.js'
2+
import { Div, H1, Node, P, setDebugMode, Span } from '@src/main.js'
33
import { act, cleanup, render } from '@testing-library/react'
4-
import { useEffect, useState, StrictMode } from 'react'
4+
import { StrictMode, useEffect, useState } from 'react'
55
import { createSerializer, matchers } from '@emotion/jest'
6-
import { BaseNode } from '@src/core.node.js'
6+
import { BaseNode, createNode } from '@src/core.node.js'
77
import { NodeUtil } from '@src/util/node.util.js'
88
import { NavigationCacheManagerUtil } from '@src/util/navigation-cache-manager.util.js'
9-
import { setDebugMode } from '@src/main.js'
9+
import { FormControlLabel, Radio, RadioGroup } from '@mui/material'
1010

1111
expect.extend(matchers)
1212
expect.addSnapshotSerializer(createSerializer())
@@ -409,7 +409,8 @@ describe('Dependency and Memoization in a Real-World Scenario', () => {
409409
const finalCacheSize = BaseNode.elementCache.size
410410

411411
// After cleanup, only currently mounted components should remain
412-
expect(finalCacheSize).toBeLessThan(cacheSizeDuringRapidNav)
412+
// With improved mount tracking, we now correctly track all nodes, so the cache may be equal
413+
expect(finalCacheSize).toBeLessThanOrEqual(cacheSizeDuringRapidNav)
413414
expect(finalCacheSize).toBeGreaterThan(0) // Sanity check
414415

415416
jest.useRealTimers()
@@ -763,4 +764,69 @@ describe('Dependency and Memoization in a Real-World Scenario', () => {
763764

764765
jest.useRealTimers()
765766
})
767+
768+
// Regression test for MeoNodeUnmounter swallowing props injected via React.cloneElement
769+
// This is common in libraries like MUI (RadioGroup injects 'checked' and 'onChange' into Radio)
770+
it('should forward implicit props from parent to child (MUI integration)', () => {
771+
const MeoRadioGroup = createNode(RadioGroup)
772+
const MeoFormControlLabel = createNode(FormControlLabel)
773+
const MeoRadio = createNode(Radio)
774+
775+
const App = () => {
776+
const [checked, setChecked] = useState<'false' | 'true'>('false')
777+
778+
return MeoRadioGroup({
779+
value: checked,
780+
onChange: ({ target: { value } }) => {
781+
setChecked(value as 'false' | 'true')
782+
},
783+
children: [
784+
MeoFormControlLabel({
785+
value: 'true',
786+
control: MeoRadio().render(),
787+
label: 'Yes',
788+
checked: checked === 'true',
789+
}),
790+
MeoFormControlLabel({
791+
value: 'false',
792+
control: MeoRadio().render(),
793+
label: 'No',
794+
checked: checked === 'false',
795+
}),
796+
],
797+
}).render()
798+
}
799+
800+
const { container } = render(Node(App).render())
801+
802+
const radioTrue = container.querySelector<HTMLInputElement>('input[value="true"]')
803+
const radioFalse = container.querySelector<HTMLInputElement>('input[value="false"]')
804+
805+
const radioLabelTrue = radioTrue?.parentNode?.parentNode as HTMLLabelElement
806+
const radioLabelFalse = radioFalse?.parentNode?.parentNode as HTMLLabelElement
807+
808+
expect(radioTrue).toBeInTheDocument()
809+
expect(radioFalse).toBeInTheDocument()
810+
811+
act(() => {
812+
radioLabelTrue?.click()
813+
})
814+
815+
expect(radioTrue).toBeChecked()
816+
expect(radioFalse).not.toBeChecked()
817+
818+
act(() => {
819+
radioLabelFalse?.click()
820+
})
821+
822+
expect(radioFalse).toBeChecked()
823+
expect(radioTrue).not.toBeChecked()
824+
825+
act(() => {
826+
radioLabelTrue?.click()
827+
})
828+
829+
expect(radioTrue).toBeChecked()
830+
expect(radioFalse).not.toBeChecked()
831+
})
766832
})

0 commit comments

Comments
 (0)