/**
 * Copyright 2018 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

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

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

  describe('Frame.waitForFunction', function () {
    it('should accept a string', async () => {
      const {page} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = 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} = getTestState();

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

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

      await page.setContent('<div></div>');
      const div = (await page.$('div'))!;
      let resolved = false;
      const waitForFunction = page
        .waitForFunction(
          (element: Element) => {
            return element.localName === 'div' && !element.parentElement;
          },
          {},
          div
        )
        .then(() => {
          return (resolved = true);
        });
      expect(resolved).toBe(false);
      await page.evaluate((element: HTMLElement) => {
        return element.remove();
      }, div);
      await waitForFunction;
    });
    it('should respect timeout', async () => {
      const {page} = 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} = 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} = 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} = 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} = 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} = 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/);
    });
  });

  describe('Page.waitForTimeout', () => {
    it('waits for the given timeout before resolving', async () => {
      const {page, server} = getTestState();
      await page.goto(server.EMPTY_PAGE);
      const startTime = Date.now();
      await page.waitForTimeout(1000);
      const endTime = Date.now();
      /* In a perfect world endTime - startTime would be exactly 1000 but we
       * expect some fluctuations and for it to be off by a little bit. So to
       * avoid a flaky test we'll make sure it waited for roughly 1 second.
       */
      expect(endTime - startTime).toBeGreaterThan(700);
      expect(endTime - startTime).toBeLessThan(1300);
    });
  });

  describe('Frame.waitForTimeout', () => {
    it('waits for the given timeout before resolving', async () => {
      const {page, server} = getTestState();
      await page.goto(server.EMPTY_PAGE);
      const frame = page.mainFrame();
      const startTime = Date.now();
      await frame.waitForTimeout(1000);
      const endTime = Date.now();
      /* In a perfect world endTime - startTime would be exactly 1000 but we
       * expect some fluctuations and for it to be off by a little bit. So to
       * avoid a flaky test we'll make sure it waited for roughly 1 second
       */
      expect(endTime - startTime).toBeGreaterThan(700);
      expect(endTime - startTime).toBeLessThan(1300);
    });
  });

  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} = 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} = 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} = getTestState();

      await page.evaluate(() => {
        // @ts-expect-error We want to remove it for the test.
        return delete window.MutationObserver;
      });
      const [handle] = await Promise.all([
        page.waitForSelector('.zombo'),
        page.setContent(`<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} = 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');
      const 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} = 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;
    });

    it('Page.waitForSelector is shortcut for main frame', async () => {
      const {page, server} = 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');
      const eHandle = await watchdog;
      expect(eHandle?.frame).toBe(page.mainFrame());
    });

    it('should run in specified frame', async () => {
      const {page, server} = 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');
      const eHandle = await waitForSelectorPromise;
      expect(eHandle?.frame).toBe(frame2);
    });

    it('should throw when frame is detached', async () => {
      const {page, server} = 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).toContain(
        'waitForFunction failed: frame got detached.'
      );
    });
    it('should survive cross-process navigation', async () => {
      const {page, server} = 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} = getTestState();

      const promise = page.waitForSelector('div', {visible: true});
      await page.setContent('<div style="display: none">text</div>');
      const 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 (visibility)', async () => {
      const {page} = getTestState();

      const promise = page.waitForSelector('div', {visible: true});
      await page.setContent('<div style="visibility: hidden">text</div>');
      const 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} = getTestState();

      const promise = page.waitForSelector('div', {visible: true});
      await page.setContent('<div style="width: 0">text</div>');
      const 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} = getTestState();

      const promise = page.waitForSelector('div#inner', {
        visible: true,
      });
      await page.setContent(
        `<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
      );
      const 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} = getTestState();

      const promise = page.waitForSelector('div', {hidden: true});
      await page.setContent(`<div style='display: block;'>text</div>`);
      const 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} = getTestState();

      const promise = page.waitForSelector('div', {hidden: true});
      await page.setContent(`<div style='display: block;'>text</div>`);
      const 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} = getTestState();

      const promise = page.waitForSelector('div', {hidden: true});
      await page.setContent('<div>text</div>');
      const 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} = getTestState();

      const promise = page.waitForSelector('div', {hidden: true});
      await page.setContent(`<div>text</div>`);
      const 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} = getTestState();

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

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

      await page.setContent(`<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).toContain(
        'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
      );
    });

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

      let divFound = false;
      const waitForSelector = page.waitForSelector('.zombo').then(() => {
        return (divFound = true);
      });
      await page.setContent(`<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} = getTestState();

      const waitForSelector = page.waitForSelector('.zombo');
      await page.setContent(`<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} = getTestState();

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

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

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

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

      let error!: Error;
      await page.waitForXPath('//div', {timeout: 10}).catch(error_ => {
        return (error = error_);
      });
      expect(error).toBeInstanceOf(TimeoutError);
      expect(error?.message).toContain('Waiting failed: 10ms exceeded');
    });
    it('should run in specified frame', async () => {
      const {page, server} = 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 waitForXPathPromise = frame2.waitForXPath('//div');
      await frame1.evaluate(addElement, 'div');
      await frame2.evaluate(addElement, 'div');
      const eHandle = await waitForXPathPromise;
      expect(eHandle?.frame).toBe(frame2);
    });
    it('should throw when frame is detached', async () => {
      const {page, server} = getTestState();

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

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

      const waitForXPath = await page.waitForXPath('//div', {hidden: true});

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

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

      const waitForXPath = await page.waitForXPath('//div', {hidden: true});

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

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

      await page.setContent(`<div>some text</div>`);
      const text = await page.waitForXPath('//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} = getTestState();

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