/**
 * @license
 * Copyright 2018 Google Inc.
 * SPDX-License-Identifier: Apache-2.0
 */

import expect from 'expect';
import {TimeoutError, ElementHandle} from 'puppeteer';
import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js';

import {
  createTimeout,
  getTestState,
  setupSeparateTestBrowserHooks,
  setupTestBrowserHooks,
} from './mocha-utils.js';
import {attachFrame, detachFrame, html, htmlRaw} from './utils.js';

describe('waittask specs', function () {
  setupTestBrowserHooks();

  describe('Frame.waitForFunction', function () {
    it('should accept a string', async () => {
      const {page} = await getTestState();

      const watchdog = page.waitForFunction('self.__FOO === 1');
      await page.evaluate(() => {
        return ((self as unknown as {__FOO: number}).__FOO = 1);
      });
      await watchdog;
    });
    it('should work when resolved right before execution context disposal', async () => {
      const {page} = await getTestState();

      await page.evaluateOnNewDocument(() => {
        return ((globalThis as any).__RELOADED = true);
      });
      await page.waitForFunction(() => {
        if (!(globalThis as any).__RELOADED) {
          window.location.reload();
          return false;
        }
        return true;
      });
    });
    it('should poll on interval', async () => {
      const {page} = await getTestState();
      const startTime = Date.now();
      const polling = 100;
      const watchdog = page.waitForFunction(
        () => {
          return (globalThis as any).__FOO === 'hit';
        },
        {polling},
      );
      await page.evaluate(() => {
        setTimeout(() => {
          (globalThis as any).__FOO = 'hit';
        }, 50);
      });
      await watchdog;
      expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
    });
    it('should poll on mutation', async () => {
      const {page} = await getTestState();

      let success = false;
      const watchdog = page
        .waitForFunction(
          () => {
            return (globalThis as any).__FOO === 'hit';
          },
          {
            polling: 'mutation',
          },
        )
        .then(() => {
          return (success = true);
        });
      await page.evaluate(() => {
        return ((globalThis as any).__FOO = 'hit');
      });
      expect(success).toBe(false);
      await page.evaluate(() => {
        return document.body.appendChild(document.createElement('div'));
      });
      await watchdog;
    });
    it('should poll on mutation async', async () => {
      const {page} = await getTestState();

      let success = false;
      const watchdog = page
        .waitForFunction(
          async () => {
            return (globalThis as any).__FOO === 'hit';
          },
          {
            polling: 'mutation',
          },
        )
        .then(() => {
          return (success = true);
        });
      await page.evaluate(async () => {
        return ((globalThis as any).__FOO = 'hit');
      });
      expect(success).toBe(false);
      await page.evaluate(async () => {
        return document.body.appendChild(document.createElement('div'));
      });
      await watchdog;
    });
    it('should poll on raf', async () => {
      const {page} = await getTestState();

      const watchdog = page.waitForFunction(
        () => {
          return (globalThis as any).__FOO === 'hit';
        },
        {
          polling: 'raf',
        },
      );
      await page.evaluate(() => {
        return ((globalThis as any).__FOO = 'hit');
      });
      await watchdog;
    });
    it('should poll on raf async', async () => {
      const {page} = await getTestState();

      const watchdog = page.waitForFunction(
        async () => {
          return (globalThis as any).__FOO === 'hit';
        },
        {
          polling: 'raf',
        },
      );
      await page.evaluate(async () => {
        return ((globalThis as any).__FOO = 'hit');
      });
      await watchdog;
    });
    it('should work with strict CSP policy', async () => {
      const {page, server} = await getTestState();

      server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
      await page.goto(server.EMPTY_PAGE);
      let error!: Error;
      await Promise.all([
        page
          .waitForFunction(
            () => {
              return (globalThis as any).__FOO === 'hit';
            },
            {
              polling: 'raf',
            },
          )
          .catch(error_ => {
            return (error = error_);
          }),
        page.evaluate(() => {
          return ((globalThis as any).__FOO = 'hit');
        }),
      ]);
      expect(error).toBeUndefined();
    });
    it('should throw negative polling interval', async () => {
      const {page} = await getTestState();

      let error!: Error;
      try {
        await page.waitForFunction(
          () => {
            return !!document.body;
          },
          {polling: -10},
        );
      } catch (error_) {
        if (isErrorLike(error_)) {
          error = error_ as Error;
        }
      }
      expect(error?.message).toContain(
        'Cannot poll with non-positive interval',
      );
    });
    it('should return the success value as a JSHandle', async () => {
      const {page} = await getTestState();

      expect(
        await (
          await page.waitForFunction(() => {
            return 5;
          })
        ).jsonValue(),
      ).toBe(5);
    });
    it('should return the window as a success value', async () => {
      const {page} = await getTestState();

      expect(
        await page.waitForFunction(() => {
          return window;
        }),
      ).toBeTruthy();
    });
    it('should accept ElementHandle arguments', async () => {
      const {page} = await getTestState();

      await page.setContent(html`<div></div>`);
      using div = (await page.$('div'))!;
      let resolved = false;
      const waitForFunction = page
        .waitForFunction(
          element => {
            return element.localName === 'div' && !element.parentElement;
          },
          {},
          div,
        )
        .then(() => {
          return (resolved = true);
        });
      expect(resolved).toBe(false);
      await page.evaluate(element => {
        return element.remove();
      }, div);
      await waitForFunction;
    });
    it('should respect timeout', async () => {
      const {page} = await getTestState();

      let error!: Error;
      await page
        .waitForFunction(
          () => {
            return false;
          },
          {timeout: 10},
        )
        .catch(error_ => {
          return (error = error_);
        });

      expect(error).toBeInstanceOf(TimeoutError);
      expect(error?.message).toContain('Waiting failed: 10ms exceeded');
    });
    it('should respect default timeout', async () => {
      const {page} = await getTestState();

      page.setDefaultTimeout(1);
      let error!: Error;
      await page
        .waitForFunction(() => {
          return false;
        })
        .catch(error_ => {
          return (error = error_);
        });
      expect(error).toBeInstanceOf(TimeoutError);
      expect(error?.message).toContain('Waiting failed: 1ms exceeded');
    });
    it('should disable timeout when its set to 0', async () => {
      const {page} = await getTestState();

      const watchdog = page.waitForFunction(
        () => {
          (globalThis as any).__counter =
            ((globalThis as any).__counter || 0) + 1;
          return (globalThis as any).__injected;
        },
        {timeout: 0, polling: 10},
      );
      await page.waitForFunction(() => {
        return (globalThis as any).__counter > 10;
      });
      await page.evaluate(() => {
        return ((globalThis as any).__injected = true);
      });
      await watchdog;
    });
    it('should survive cross-process navigation', async () => {
      const {page, server} = await getTestState();

      let fooFound = false;
      const waitForFunction = page
        .waitForFunction(() => {
          return (globalThis as unknown as {__FOO: number}).__FOO === 1;
        })
        .then(() => {
          return (fooFound = true);
        });
      await page.goto(server.EMPTY_PAGE);
      expect(fooFound).toBe(false);
      await page.reload();
      expect(fooFound).toBe(false);
      await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
      expect(fooFound).toBe(false);
      await page.evaluate(() => {
        return ((globalThis as any).__FOO = 1);
      });
      await waitForFunction;
      expect(fooFound).toBe(true);
    });
    it('should survive navigations', async () => {
      const {page, server} = await getTestState();

      const watchdog = page.waitForFunction(() => {
        return (globalThis as any).__done;
      });
      await page.goto(server.EMPTY_PAGE);
      await page.goto(server.PREFIX + '/consolelog.html');
      await page.evaluate(() => {
        return ((globalThis as any).__done = true);
      });
      await watchdog;
    });
    it('should be cancellable', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.EMPTY_PAGE);
      const abortController = new AbortController();
      const task = page.waitForFunction(
        () => {
          return (globalThis as any).__done;
        },
        {
          signal: abortController.signal,
        },
      );
      abortController.abort();
      await expect(task).rejects.toThrow(/aborted/);
    });
    it('can start multiple tasks without node warnings', async () => {
      const {page} = await getTestState();
      let warning: Error | undefined;
      const warningHandler: NodeJS.WarningListener = w => {
        warning = w;
      };
      process.on('warning', warningHandler);
      process.setMaxListeners(1);
      const abortController = new AbortController();
      try {
        for (let i = 0; i < 2; i++) {
          await page.waitForFunction(
            () => {
              return true;
            },
            {
              signal: abortController.signal,
            },
          );
        }
      } finally {
        process.setMaxListeners(10);
      }
      process.off('warning', warningHandler);
      expect(warning?.stack).toBe(undefined);
    });
  });

  describe('Frame.waitForSelector', function () {
    const addElement = (tag: string) => {
      return document.body.appendChild(document.createElement(tag));
    };

    it('should immediately resolve promise if node exists', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.EMPTY_PAGE);
      const frame = page.mainFrame();
      await frame.waitForSelector('*');
      await frame.evaluate(addElement, 'div');
      await frame.waitForSelector('div');
    });

    it('should be cancellable', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.EMPTY_PAGE);
      const abortController = new AbortController();
      const task = page.waitForSelector('wrong', {
        signal: abortController.signal,
      });
      abortController.abort();
      await expect(task).rejects.toThrow(/aborted/);
    });

    it('should work with removed MutationObserver', async () => {
      const {page} = await getTestState();

      await page.evaluate(() => {
        // @ts-expect-error We want to remove it for the test.
        delete window.MutationObserver;
      });
      const [handle] = await Promise.all([
        page.waitForSelector('.zombo'),
        page.setContent(html`<div class="zombo">anything</div>`),
      ]);
      expect(
        await page.evaluate(x => {
          return x?.textContent;
        }, handle),
      ).toBe('anything');
    });

    it('should resolve promise when node is added', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.EMPTY_PAGE);
      const frame = page.mainFrame();
      const watchdog = frame.waitForSelector('div');
      await frame.evaluate(addElement, 'br');
      await frame.evaluate(addElement, 'div');
      using eHandle = (await watchdog)!;
      const tagName = await (await eHandle.getProperty('tagName')).jsonValue();
      expect(tagName).toBe('DIV');
    });

    it('should work when node is added through innerHTML', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.EMPTY_PAGE);
      const watchdog = page.waitForSelector('h3 div');
      await page.evaluate(addElement, 'span');
      await page.evaluate(() => {
        return (document.querySelector('span')!.innerHTML =
          '<h3><div></div></h3>');
      });
      await watchdog;
    });

    // MutationPoller currently does not support shadow DOM.
    // See https://github.com/puppeteer/puppeteer/issues/13163.
    it('should work when node is added in a shadow root', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.EMPTY_PAGE);
      const watcher = page.waitForSelector('div >>> h1');
      await page.evaluate(addElement, 'div');
      await expect(
        Promise.race([watcher, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await page.evaluate(() => {
        const host = document.querySelector('div')!;
        const shadow = host.attachShadow({mode: 'open'});
        const h1 = document.createElement('h1');
        h1.textContent = 'inside';
        shadow.appendChild(h1);
      });
      using element = await watcher;
      expect(
        await element!.evaluate(el => {
          return el.textContent;
        }),
      ).toBe('inside');
    });

    it('should work for selector with a pseudo class', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.EMPTY_PAGE);
      const watchdog = page.waitForSelector('input:focus');
      await expect(
        Promise.race([watchdog, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await page.setContent(html`<input></input>`);
      await page.click('input');
      await watchdog;
    });

    it('Page.waitForSelector is shortcut for main frame', async () => {
      const {page, server} = await getTestState();

      await page.goto(server.EMPTY_PAGE);
      await attachFrame(page, 'frame1', server.EMPTY_PAGE);
      const otherFrame = page.frames()[1]!;
      const watchdog = page.waitForSelector('div');
      await otherFrame.evaluate(addElement, 'div');
      await page.evaluate(addElement, 'div');
      using eHandle = await watchdog;
      expect(eHandle?.frame).toBe(page.mainFrame());
    });

    it('should run in specified frame', async () => {
      const {page, server} = await getTestState();

      await attachFrame(page, 'frame1', server.EMPTY_PAGE);
      await attachFrame(page, 'frame2', server.EMPTY_PAGE);
      const frame1 = page.frames()[1]!;
      const frame2 = page.frames()[2]!;
      const waitForSelectorPromise = frame2.waitForSelector('div');
      await frame1.evaluate(addElement, 'div');
      await frame2.evaluate(addElement, 'div');
      using eHandle = await waitForSelectorPromise;
      expect(eHandle?.frame).toBe(frame2);
    });

    it('should throw when frame is detached', async () => {
      const {page, server} = await getTestState();

      await attachFrame(page, 'frame1', server.EMPTY_PAGE);
      const frame = page.frames()[1]!;
      let waitError: Error | undefined;
      const waitPromise = frame.waitForSelector('.box').catch(error => {
        return (waitError = error);
      });
      await detachFrame(page, 'frame1');
      await waitPromise;
      expect(waitError).toBeTruthy();
      expect(waitError?.message).toBe('Waiting for selector `.box` failed');
    });
    it('should survive cross-process navigation', async () => {
      const {page, server} = await getTestState();

      let boxFound = false;
      const waitForSelector = page.waitForSelector('.box').then(() => {
        return (boxFound = true);
      });
      await page.goto(server.EMPTY_PAGE);
      expect(boxFound).toBe(false);
      await page.reload();
      expect(boxFound).toBe(false);
      await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
      await waitForSelector;
      expect(boxFound).toBe(true);
    });
    it('should wait for element to be visible (display)', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div', {visible: true});
      await page.setContent(html`<div style="display: none">text</div>`);
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        e.style.removeProperty('display');
      });
      await expect(promise).resolves.toBeTruthy();
    });
    it('should wait for element to be visible (without DOM mutations)', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div', {visible: true});
      await page.setContent(
        html`<style>
            div {
              display: none;
            }
          </style>
          <div>text</div>`,
      );
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      expect(element).toBeTruthy();
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await page.evaluate(() => {
        const extraSheet = new CSSStyleSheet();
        extraSheet.replaceSync('div { display: block; }');
        document.adoptedStyleSheets = [
          ...document.adoptedStyleSheets,
          extraSheet,
        ];
      });
      await expect(promise).resolves.toBeTruthy();
    });
    it('should wait for element to be visible (visibility)', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div', {visible: true});
      await page.setContent(html`<div style="visibility: hidden">text</div>`);
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        e.style.setProperty('visibility', 'collapse');
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        e.style.removeProperty('visibility');
      });
      await expect(promise).resolves.toBeTruthy();
    });
    it('should wait for element to be visible (bounding box)', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div', {visible: true});
      await page.setContent(html`<div style="width: 0">text</div>`);
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        e.style.setProperty('height', '0');
        e.style.removeProperty('width');
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        e.style.removeProperty('height');
      });
      await expect(promise).resolves.toBeTruthy();
    });
    it('should wait for element to be visible recursively', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div#inner', {
        visible: true,
      });
      await page.setContent(
        html`<div style="display: none; visibility: hidden;">
          <div id="inner">hi</div>
        </div>`,
      );
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        return e.style.removeProperty('display');
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        return e.style.removeProperty('visibility');
      });
      await expect(promise).resolves.toBeTruthy();
    });
    it('should wait for element to be hidden (visibility)', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div', {hidden: true});
      await page.setContent(html`<div style="display: block;">text</div>`);
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        return e.style.setProperty('visibility', 'hidden');
      });
      await expect(promise).resolves.toBeTruthy();
    });
    it('should wait for element to be hidden (display)', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div', {hidden: true});
      await page.setContent(html`<div style="display: block;">text</div>`);
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        return e.style.setProperty('display', 'none');
      });
      await expect(promise).resolves.toBeTruthy();
    });
    it('should wait for element to be hidden (bounding box)', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div', {hidden: true});
      await page.setContent(html`<div>text</div>`);
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      await expect(
        Promise.race([promise, createTimeout(40)]),
      ).resolves.toBeFalsy();
      await element.evaluate(e => {
        e.style.setProperty('height', '0');
      });
      await expect(promise).resolves.toBeTruthy();
    });
    it('should wait for element to be hidden (removal)', async () => {
      const {page} = await getTestState();

      const promise = page.waitForSelector('div', {hidden: true});
      await page.setContent(html`<div>text</div>`);
      using element = await page.evaluateHandle(() => {
        return document.getElementsByTagName('div')[0]!;
      });
      await expect(
        Promise.race([promise, createTimeout(40, true)]),
      ).resolves.toBeTruthy();
      await element.evaluate(e => {
        e.remove();
      });
      await expect(promise).resolves.toBeFalsy();
    });
    it('should return null if waiting to hide non-existing element', async () => {
      const {page} = await getTestState();

      using handle = await page.waitForSelector('non-existing', {
        hidden: true,
      });
      expect(handle).toBe(null);
    });
    it('should respect timeout', async () => {
      const {page} = await getTestState();

      let error!: Error;
      await page.waitForSelector('div', {timeout: 10}).catch(error_ => {
        return (error = error_);
      });
      expect(error).toBeInstanceOf(TimeoutError);
      expect(error?.message).toBe('Waiting for selector `div` failed');
    });
    it('should have an error message specifically for awaiting an element to be hidden', async () => {
      const {page} = await getTestState();

      await page.setContent(html`<div>text</div>`);
      let error!: Error;
      await page
        .waitForSelector('div', {hidden: true, timeout: 10})
        .catch(error_ => {
          return (error = error_);
        });
      expect(error).toBeTruthy();
      expect(error?.message).toBe('Waiting for selector `div` failed');
    });

    it('should respond to node attribute mutation', async () => {
      const {page} = await getTestState();

      let divFound = false;
      const waitForSelector = page.waitForSelector('.zombo').then(() => {
        return (divFound = true);
      });
      await page.setContent(html`<div class="notZombo"></div>`);
      expect(divFound).toBe(false);
      await page.evaluate(() => {
        return (document.querySelector('div')!.className = 'zombo');
      });
      expect(await waitForSelector).toBe(true);
    });
    it('should return the element handle', async () => {
      const {page} = await getTestState();

      const waitForSelector = page.waitForSelector('.zombo');
      await page.setContent(html`<div class="zombo">anything</div>`);
      expect(
        await page.evaluate(
          x => {
            return x?.textContent;
          },
          await waitForSelector,
        ),
      ).toBe('anything');
    });
    it('should have correct stack trace for timeout', async () => {
      const {page} = await getTestState();

      let error!: Error;
      await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
        return (error = error_);
      });
      expect(error?.stack).toContain('Waiting for selector `.zombo` failed');
      // The extension is ts here as Mocha maps back via sourcemaps.
      expect(error?.stack).toContain('waittask.spec.ts');
    });

    describe('xpath', function () {
      const addElement = (tag: string) => {
        return document.body.appendChild(document.createElement(tag));
      };

      it('should support some fancy xpath', async () => {
        const {page} = await getTestState();

        await page.setContent(htmlRaw`<p>red herring</p><p>hello  world  </p>`);
        const waitForSelector = page.waitForSelector(
          'xpath/.//p[normalize-space(.)="hello world"]',
        );
        expect(
          await page.evaluate(
            x => {
              return x?.textContent;
            },
            await waitForSelector,
          ),
        ).toBe('hello  world  ');
      });
      it('should respect timeout', async () => {
        const {page} = await getTestState();

        let error!: Error;
        await page
          .waitForSelector('xpath/.//div', {timeout: 10})
          .catch(error_ => {
            return (error = error_);
          });
        expect(error).toBeInstanceOf(TimeoutError);
        expect(error?.message).toBe('Waiting for selector `.//div` failed');
      });
      it('should run in specified frame', async () => {
        const {page, server} = await getTestState();

        await attachFrame(page, 'frame1', server.EMPTY_PAGE);
        await attachFrame(page, 'frame2', server.EMPTY_PAGE);
        const frame1 = page.frames()[1]!;
        const frame2 = page.frames()[2]!;
        const waitForSelector = frame2.waitForSelector('xpath/.//div');
        await frame1.evaluate(addElement, 'div');
        await frame2.evaluate(addElement, 'div');
        using eHandle = await waitForSelector;
        expect(eHandle?.frame).toBe(frame2);
      });
      it('should throw when frame is detached', async () => {
        const {page, server} = await getTestState();

        await attachFrame(page, 'frame1', server.EMPTY_PAGE);
        const frame = page.frames()[1]!;
        let waitError: Error | undefined;
        const waitPromise = frame
          .waitForSelector('xpath/.//*[@class="box"]')
          .catch(error => {
            return (waitError = error);
          });
        await detachFrame(page, 'frame1');
        await waitPromise;
        expect(waitError).toBeTruthy();
        expect(waitError?.message).toBe(
          'Waiting for selector `.//*[@class="box"]` failed',
        );
      });
      it('hidden should wait for display: none', async () => {
        const {page} = await getTestState();

        let divHidden = false;
        await page.setContent(html`<div style="display: block;">text</div>`);
        const waitForSelector = page
          .waitForSelector('xpath/.//div', {hidden: true})
          .then(() => {
            return (divHidden = true);
          });
        await page.waitForSelector('xpath/.//div'); // do a round trip
        expect(divHidden).toBe(false);
        await page.evaluate(() => {
          return document
            .querySelector('div')
            ?.style.setProperty('display', 'none');
        });
        expect(await waitForSelector).toBe(true);
        expect(divHidden).toBe(true);
      });
      it('hidden should return null if the element is not found', async () => {
        const {page} = await getTestState();

        using waitForSelector = await page.waitForSelector('xpath/.//div', {
          hidden: true,
        });

        expect(waitForSelector).toBe(null);
      });
      it('hidden should return an empty element handle if the element is found', async () => {
        const {page} = await getTestState();

        await page.setContent(html`<div style="display: none;">text</div>`);

        using waitForSelector = await page.waitForSelector('xpath/.//div', {
          hidden: true,
        });

        expect(waitForSelector).toBeInstanceOf(ElementHandle);
      });
      it('should return the element handle', async () => {
        const {page} = await getTestState();

        const waitForSelector = page.waitForSelector(
          'xpath/.//*[@class="zombo"]',
        );
        await page.setContent(html`<div class="zombo">anything</div>`);
        expect(
          await page.evaluate(
            x => {
              return x?.textContent;
            },
            await waitForSelector,
          ),
        ).toBe('anything');
      });
      it('should allow you to select a text node', async () => {
        const {page} = await getTestState();

        await page.setContent(html`<div>some text</div>`);
        using text = await page.waitForSelector('xpath/.//div/text()');
        expect(await (await text!.getProperty('nodeType')!).jsonValue()).toBe(
          3 /* Node.TEXT_NODE */,
        );
      });
      it('should allow you to select an element with single slash', async () => {
        const {page} = await getTestState();

        await page.setContent(html`<div>some text</div>`);
        const waitForSelector = page.waitForSelector('xpath/html/body/div');
        expect(
          await page.evaluate(
            x => {
              return x?.textContent;
            },
            await waitForSelector,
          ),
        ).toBe('some text');
      });
    });
  });

  describe('protocol timeout', () => {
    const state = setupSeparateTestBrowserHooks({
      protocolTimeout: 3000,
    });

    it('should error if underyling protocol command times out with raf polling', async () => {
      let error!: Error;
      await state.page
        .waitForFunction(
          () => {
            return false;
          },
          {timeout: 6000},
        )
        .catch(error_ => {
          return (error = error_);
        });

      expect(error).toBeInstanceOf(Error);
      expect(error.message).toBe('Waiting failed');
      expect(error.stack).toContain('waittask.spec.ts');
      expect(error.cause).toBeInstanceOf(Error);
    });
  });
});
