@@ -3,165 +3,6 @@ import { test, expect } from '@playwright/test'
33test ( 'Only two ListItems should not rerender when the highlighted item changes' , async ( {
44 page,
55} ) => {
6- await page . route ( '/' , async ( route ) => {
7- const request = route . request ( )
8- if ( request . resourceType ( ) !== 'document' ) return route . continue ( )
9- const response = await route . fetch ( )
10-
11- let html = await response . text ( )
12- const scriptToInject = `
13- <script>
14- let internals
15- const PerformedWorkFlag = 0b000000000000000000000000001
16- const UserCodeFiberTags = new Set([0, 1, 9, 11, 15])
17-
18- let activeFallbackComponentNames = null
19-
20- function isUserCodeFiberTag(tag) {
21- return UserCodeFiberTags.has(tag)
22- }
23-
24- function getDisplayNameFromType(type) {
25- if (!type) return null
26-
27- if (typeof type === 'function') {
28- return type.displayName || type.name || null
29- }
30-
31- if (typeof type === 'object') {
32- if (typeof type.displayName === 'string' && type.displayName) {
33- return type.displayName
34- }
35-
36- if (typeof type.render === 'function') {
37- return type.render.displayName || type.render.name || null
38- }
39-
40- if (type.type) {
41- return getDisplayNameFromType(type.type)
42- }
43- }
44-
45- return null
46- }
47-
48- function getFiberDisplayName(fiber) {
49- return getDisplayNameFromType(fiber.type) || getDisplayNameFromType(fiber.elementType)
50- }
51-
52- function didFiberRender(previousFiber, nextFiber) {
53- if (!previousFiber) return true
54- return (nextFiber.flags & PerformedWorkFlag) === PerformedWorkFlag
55- }
56-
57- function collectRenderedComponentsFromCommit(root) {
58- if (!activeFallbackComponentNames || !root?.current) return
59-
60- const stack = [root.current]
61- while (stack.length > 0) {
62- const fiber = stack.pop()
63- if (!fiber) continue
64-
65- if (fiber.sibling) stack.push(fiber.sibling)
66- if (fiber.child) stack.push(fiber.child)
67-
68- if (!isUserCodeFiberTag(fiber.tag)) continue
69- if (!didFiberRender(fiber.alternate, fiber)) continue
70-
71- const componentName = getFiberDisplayName(fiber) ?? 'Anonymous'
72- activeFallbackComponentNames.push(componentName)
73- }
74- }
75-
76- function enhanceExistingHook(existingHook) {
77- const originalInject = existingHook.inject
78- const originalCommitFiberRoot = existingHook.onCommitFiberRoot
79-
80- existingHook.inject = (injectedInternals) => {
81- internals = injectedInternals
82-
83- // Returning a number as React expects a renderer ID
84- return originalInject?.call(existingHook, injectedInternals) ?? 1
85- }
86-
87- existingHook.onCommitFiberRoot = (...args) => {
88- const [, root] = args
89- collectRenderedComponentsFromCommit(root)
90- return originalCommitFiberRoot?.apply(existingHook, args)
91- }
92-
93- return existingHook
94- }
95-
96- function createMinimalHook() {
97- return {
98- renderers: [],
99- supportsFiber: true,
100- inject: (injectedInternals) => {
101- internals = injectedInternals
102- return 1 // Returning a number as React expects a renderer ID
103- },
104- onCommitFiberRoot: (_rendererId, root) => {
105- collectRenderedComponentsFromCommit(root)
106- },
107- onCommitFiberUnmount: () => {},
108- }
109- }
110-
111- async function getComponentCalls(cb) {
112- const componentNames = []
113- if (!internals) {
114- throw new Error('🚨 React DevTools is not available')
115- }
116-
117- const supportsInjectedProfilingHooks =
118- typeof internals.injectProfilingHooks === 'function'
119-
120- if (supportsInjectedProfilingHooks) {
121- internals.enableProfilerTimer = true
122- internals.enableProfilerCommitHooks = true
123- internals.injectProfilingHooks({
124- markComponentRenderStarted: (fiber) => {
125- componentNames.push(fiber.type.name || 'Anonymous')
126- },
127- })
128- } else {
129- activeFallbackComponentNames = componentNames
130- }
131-
132- try {
133- await cb()
134- } finally {
135- if (supportsInjectedProfilingHooks) {
136- internals.enableProfilerTimer = false
137- internals.enableProfilerCommitHooks = false
138- internals.injectProfilingHooks(null)
139- } else {
140- activeFallbackComponentNames = null
141- }
142- }
143-
144- return componentNames
145- }
146-
147- window.getComponentCalls = getComponentCalls
148-
149- if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
150- window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = enhanceExistingHook(
151- window.__REACT_DEVTOOLS_GLOBAL_HOOK__,
152- )
153- } else {
154- window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = createMinimalHook()
155- }
156- </script>
157- `
158- html = html . replace ( '<head>' , `<head>${ scriptToInject } ` )
159- await route . fulfill ( {
160- body : html ,
161- headers : { 'content-type' : 'text/html' } ,
162- } )
163- } )
164-
1656 await page . goto ( '/' )
1667 await page . waitForLoadState ( 'networkidle' )
1678
@@ -180,28 +21,31 @@ test('Only two ListItems should not rerender when the highlighted item changes',
18021 )
18122 } )
18223
183- // go to the next item, we should now have two that render, the old one to unhighlight it and the new one to highlight it
184- const calledComponents : Array < string > = await page . evaluate ( ( ) =>
185- ( window as any ) . getComponentCalls ( ( ) => {
186- const input = document . querySelector ( 'input' )
187- if ( ! input ) {
188- throw new Error ( '🚨 could not find the input' )
189- }
190- input . dispatchEvent (
191- new KeyboardEvent ( 'keydown' , {
192- key : 'ArrowDown' ,
193- keyCode : 40 ,
194- bubbles : true ,
195- } ) ,
196- )
197- } ) ,
198- )
24+ // go to the next item and verify only two list items change highlighted state
25+ const changedHighlights = await page . evaluate ( async ( ) => {
26+ const items = Array . from ( document . querySelectorAll ( 'li' ) )
27+ const previousMarkup = items . map ( ( item ) => item . outerHTML )
28+
29+ const input = document . querySelector ( 'input' )
30+ if ( ! input ) {
31+ throw new Error ( '🚨 could not find the input' )
32+ }
33+ input . dispatchEvent (
34+ new KeyboardEvent ( 'keydown' , {
35+ key : 'ArrowDown' ,
36+ keyCode : 40 ,
37+ bubbles : true ,
38+ } ) ,
39+ )
40+ await new Promise ( ( resolve ) => requestAnimationFrame ( resolve ) )
41+
42+ const nextMarkup = items . map ( ( item ) => item . outerHTML )
19943
200- // memo can change the name of the components, so we'll be more generous with a regex
201- const listItemRenders = calledComponents . filter ( ( c ) => / L i s t I t e m / i . test ( c ) )
44+ return nextMarkup . filter ( ( markup , index ) => markup !== previousMarkup [ index ] ) . length
45+ } )
20246
20347 expect (
204- listItemRenders ,
205- '🚨 Only two ListItems should render when changing the highlighted item. The first is rerendered to un-highlight it and the second is rerendered to highlight it. Make sure your comparator is correct .' ,
206- ) . toHaveLength ( 2 )
48+ changedHighlights ,
49+ '🚨 Only two ListItems should change highlighted state when moving from one highlighted item to the next .' ,
50+ ) . toBe ( 2 )
20751} )
0 commit comments