diff --git a/src/dom.ts b/src/dom.ts index 7943b2fedc..0f800108c2 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -321,6 +321,8 @@ export class ElementHandle extends js.JSHandle { async _performPointerAction(progress: Progress, action: (point: types.Point) => Promise, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:nothittarget' | 'done'> { const { force = false, position } = options; + if ((options as any).__testHookBeforeStable) + await (options as any).__testHookBeforeStable(); if (!force) { const result = await this._waitForDisplayedAtStablePositionAndEnabled(progress); if (result !== 'done') diff --git a/test/assets/react.html b/test/assets/react.html index 01a7144a42..0377b489d3 100644 --- a/test/assets/react.html +++ b/test/assets/react.html @@ -8,5 +8,26 @@ window.e = React.createElement; window.reactRoot = document.querySelector('.react-root'); window.renderComponent = c => ReactDOM.render(c, window.reactRoot); + + window.MyButton = class MyButton extends React.Component { + constructor(props) { + super(props); + this.state = { hovered: false }; + } + render() { + return e('button', { + disabled: !!this.props.disabled, + onClick: () => { + window[this.props.name] = true; + }, + onMouseEnter: () => { + if (this.props.renameOnHover) + this.setState({ hovered: true }); + if (this.props.onHover) + this.props.onHover(); + }, + }, this.state.hovered ? 'Hovered' : this.props.name); + } + }; diff --git a/test/click.spec.js b/test/click.spec.js index 5df0d365e7..a61f560a31 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -748,26 +748,87 @@ describe('Page.click', function() { it.fail(true)('should retarget when element is recycled during hit testing', async ({page, server}) => { await page.goto(server.PREFIX + '/react.html'); await page.evaluate(() => { - class MyButton extends React.Component { - render() { - return e('button', { onClick: () => window[this.props.name] = true }, this.props.name); - } - } - window.TwoButtons = class TwoButtons extends React.Component { - render() { - const buttons = this.props.names.map(name => e(MyButton, { name })); - return e('div', {}, buttons); - } - } - renderComponent(e(TwoButtons, { names: ['button1', 'button2'] })); + renderComponent(e('div', {}, [e(MyButton, { name: 'button1' }), e(MyButton, { name: 'button2' })] )); }); const __testHookAfterStable = () => page.evaluate(() => { - renderComponent(e(TwoButtons, { names: ['button2', 'button1'] })); + window.counter = (window.counter || 0) + 1; + if (window.counter === 1) + renderComponent(e('div', {}, [e(MyButton, { name: 'button2' }), e(MyButton, { name: 'button1' })] )); }); await page.click('text=button1', { __testHookAfterStable }); expect(await page.evaluate(() => window.button1)).toBe(true); expect(await page.evaluate(() => window.button2)).toBe(undefined); }); + it.fail(true)('should report that selector does not match anymore', async ({page, server}) => { + await page.goto(server.PREFIX + '/react.html'); + await page.evaluate(() => { + renderComponent(e('div', {}, [e(MyButton, { name: 'button1' }), e(MyButton, { name: 'button2' })] )); + }); + const __testHookAfterStable = () => page.evaluate(() => { + window.counter = (window.counter || 0) + 1; + if (window.counter === 1) + renderComponent(e('div', {}, [e(MyButton, { name: 'button2' }), e(MyButton, { name: 'button1' })] )); + else + renderComponent(e('div', {}, [])); + }); + const error = await page.dblclick('text=button1', { __testHookAfterStable, timeout: 3000 }).catch(e => e); + expect(await page.evaluate(() => window.button1)).toBe(undefined); + expect(await page.evaluate(() => window.button2)).toBe(undefined); + expect(error.message).toContain('Timeout 3000ms exceeded during page.dblclick.'); + expect(error.message).toContain('element does not match the selector anymore'); + }); + it.fail(true)('should retarget when element is recycled before enabled check', async ({page, server}) => { + await page.goto(server.PREFIX + '/react.html'); + await page.evaluate(() => { + renderComponent(e('div', {}, [e(MyButton, { name: 'button1' }), e(MyButton, { name: 'button2', disabled: true })] )); + }); + const __testHookBeforeStable = () => page.evaluate(() => { + window.counter = (window.counter || 0) + 1; + if (window.counter === 1) + renderComponent(e('div', {}, [e(MyButton, { name: 'button2', disabled: true }), e(MyButton, { name: 'button1' })] )); + }); + await page.click('text=button1', { __testHookBeforeStable }); + expect(await page.evaluate(() => window.button1)).toBe(true); + expect(await page.evaluate(() => window.button2)).toBe(undefined); + }); + it.fail(true)('should not retarget the handle when element is recycled', async ({page, server}) => { + await page.goto(server.PREFIX + '/react.html'); + await page.evaluate(() => { + renderComponent(e('div', {}, [e(MyButton, { name: 'button1' }), e(MyButton, { name: 'button2', disabled: true })] )); + }); + const __testHookBeforeStable = () => page.evaluate(() => { + window.counter = (window.counter || 0) + 1; + if (window.counter === 1) + renderComponent(e('div', {}, [e(MyButton, { name: 'button2', disabled: true }), e(MyButton, { name: 'button1' })] )); + }); + const handle = await page.$('text=button1'); + const error = await handle.click({ __testHookBeforeStable, timeout: 3000 }).catch(e => e); + expect(await page.evaluate(() => window.button1)).toBe(undefined); + expect(await page.evaluate(() => window.button2)).toBe(undefined); + expect(error.message).toContain('Timeout 3000ms exceeded during elementHandle.click.'); + expect(error.message).toContain('element is disabled - waiting'); + }); + it('should not retarget when element changes on hover', async ({page, server}) => { + await page.goto(server.PREFIX + '/react.html'); + await page.evaluate(() => { + renderComponent(e('div', {}, [e(MyButton, { name: 'button1', renameOnHover: true }), e(MyButton, { name: 'button2' })] )); + }); + await page.click('text=button1'); + expect(await page.evaluate(() => window.button1)).toBe(true); + expect(await page.evaluate(() => window.button2)).toBe(undefined); + }); + it('should not retarget when element is recycled on hover', async ({page, server}) => { + await page.goto(server.PREFIX + '/react.html'); + await page.evaluate(() => { + function shuffle() { + renderComponent(e('div', {}, [e(MyButton, { name: 'button2' }), e(MyButton, { name: 'button1' })] )); + } + renderComponent(e('div', {}, [e(MyButton, { name: 'button1', onHover: shuffle }), e(MyButton, { name: 'button2' })] )); + }); + await page.click('text=button1'); + expect(await page.evaluate(() => window.button1)).toBe(undefined); + expect(await page.evaluate(() => window.button2)).toBe(true); + }); it('should click the button when window.innerWidth is corrupted', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); await page.evaluate(() => window.innerWidth = 0);