Playwright组件测试:无法访问React Portal挂载的元素
解决Playwright无法访问React Portal挂载元素的问题
问题场景
在React组件中,formEl支持以普通模式和React Portal模式挂载。Playwright组件测试可以正常访问普通模式下的元素,但无法访问通过Portal挂载到document.body的元素。
组件代码示例
function App() { const [todos, setTodos] = useState<Todo[]>([{id: 1, title: "Buy Milk"}]) const [formMode, setFormMode] = useState<"portal" | 'normal' | 'none'>('none') const formEl = <div className={'form'}> <form onSubmit={e => { e.preventDefault() setFormMode('none') const data = e.currentTarget.elements.todo.value; setTodos(prevVal => [...prevVal, {id: Date.now(), title: data}]) }}> <input name={'todo'} placeholder={'Enter Todo'}/> <button type={'submit'}>Submit</button> </form> </div>; return ( <div> <div className={'btn-group'}> <button onClick={() => setFormMode('portal')}>Add Todo via Portal</button> <button onClick={() => setFormMode('normal')}>Add Todo Normal Mode</button> </div> <div className={'todos'}> {todos.map((todo) => (<div className={'todo-item'} key={todo.id}> {todo.title} </div>))} </div> {formMode === 'portal' && createPortal( formEl, document.body )} {formMode === 'normal' && formEl} </div> ) }
原测试代码
test('normal mode', async ({ mount }) => { const component = await mount(<App />); await component.getByRole('button', { name: 'Add Todo Normal Mode' }).click(); await expect(component.getByPlaceholder('Enter Todo')).toBeVisible() await component.getByPlaceholder('Enter Todo').fill("Learn Playwright") await component.getByRole('button', {name: 'submit'}).click() await expect(component.getByText("Learn Playwright")).toBeVisible() }); test('portal mode', async ({ mount }) => { const component = await mount(<App />); await component.getByRole('button', { name: 'Add Todo via Portal' }).click(); await expect(component.getByPlaceholder('Enter Todo')).toBeVisible() await component.getByPlaceholder('Enter Todo').fill("Learn Playwright") await component.getByRole('button', {name: 'submit'}).click() await expect(component.getByText("Learn Playwright")).toBeVisible() });
解决方案
1. 使用全局page对象查询元素
React Portal将元素挂载到document.body,超出了组件实例的默认查询范围。改用Playwright的全局page对象可以查询整个页面的元素,包括body下的Portal内容:
test('portal mode', async ({ mount, page }) => { const component = await mount(<App />); await component.getByRole('button', { name: 'Add Todo via Portal' }).click(); // 通过page查询Portal渲染的元素 await expect(page.getByPlaceholder('Enter Todo')).toBeVisible() await page.getByPlaceholder('Enter Todo').fill("Learn Playwright") await page.getByRole('button', { name: 'Submit' }).click() // 组件内的todo列表仍可通过component查询 await expect(component.getByText("Learn Playwright")).toBeVisible() });
2. 自定义mount的根元素
在mount组件时指定root为document.body,让组件本身也挂载到body节点,此时组件实例的查询范围会包含body下的Portal元素:
test('portal mode with custom root', async ({ mount }) => { const component = await mount(<App />, { root: document.body }); await component.getByRole('button', { name: 'Add Todo via Portal' }).click(); // 直接用component查询即可 await expect(component.getByPlaceholder('Enter Todo')).toBeVisible() await component.getByPlaceholder('Enter Todo').fill("Learn Playwright") await component.getByRole('button', { name: 'Submit' }).click() await expect(component.getByText("Learn Playwright")).toBeVisible() });
3. 通过组件的ownerDocument获取body查询
利用组件实例的ownerDocument()方法获取文档对象,进而查询body内的Portal元素:
test('portal mode via ownerDocument', async ({ mount }) => { const component = await mount(<App />); await component.getByRole('button', { name: 'Add Todo via Portal' }).click(); const body = component.ownerDocument().body; // 通过body查询Portal元素 await expect(body.getByPlaceholder('Enter Todo')).toBeVisible() await body.getByPlaceholder('Enter Todo').fill("Learn Playwright") await body.getByRole('button', { name: 'Submit' }).click() await expect(component.getByText("Learn Playwright")).toBeVisible() });
原因说明
Playwright组件测试中,mount返回的组件实例默认仅在自身的根DOM节点范围内查询元素。而React Portal会将元素渲染到DOM树的其他位置(如document.body),超出了组件实例的查询边界,因此直接用component.getBy...无法找到这些元素。
内容的提问来源于stack exchange,提问作者Rajesh Batth
相关产品推荐
相关产品推荐

