You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 12:41:08